Debugging Encodings
Text Encoding and Unicode
Course 1 · Chapter 10 · Debugging Encodings
You now know what text is (Text Encoding and Unicode 1), how UTF-8 and its rivals are built (Text Encoding and Unicode 2, 3 and 4), why equal-looking strings differ (Text Encoding and Unicode 5 and 6), and where encodings are declared or lost (Text Encoding and Unicode 7 and 8), and the security angle (Text Encoding and Unicode 9). This final chapter turns that knowledge into a repeatable debugging method, builds a small toolkit, and then solves a realistic damaged string end to end.
The Six-Step Method
- Look at the bytes and code points, not the rendered text. Print
ascii(s)and the UTF-8 hex. A screen hides invisible characters and may itself be using the wrong font or encoding. - Recognise the pattern. Most damage has a recognisable shape (table below).
- Decide: reversible or lost? If the original bytes are still all present (just misread) you can reverse it. If a step replaced characters with
?orU+FFFD, the information is gone and you must re-fetch the source. - Find the boundary. Text passes through boundaries (file, network, database, program). Test the text after each one and find the first where it stops matching.
- Fix at the source. Declare or set the right encoding at the faulty boundary. Do not scatter repairs through the program.
- Prevent. Always pass an encoding, normalize on input, and keep test data with non-ASCII characters, emoji and a decomposed accent in it.
Step 1: Look at the Real Data
The repr line shows exactly which code points are present (including any invisible ones, Text Encoding and Unicode 9), and the hex line shows the bytes, which is what you compare with what you expect from a file or a network capture.
Step 2: Recognise the Pattern
Take one clean string and damage it in the common ways, to learn what each looks like:
The Latin-1 version of this damage looks much the same but with invisible control characters in place of the Windows-1252 punctuation. Use this as a field guide:
| You see | Likely cause | Reversible? |
|---|---|---|
| é è ö and ’ “ | UTF-8 bytes read as Windows-1252 or Latin-1 (Text Encoding and Unicode 3) | Usually yes, if no byte was replaced; the last quote above ended in U+FFFD (the replacement character), because byte 9D is undefined in Windows-1252, so that one is lost |
|  at the start | A UTF-8 byte order mark read as text, often after two layers (Text Encoding and Unicode 4) | Yes: decode with utf-8-sig |
| Ã followed by more Ã, Â and ‚ | Several layers of the same mistake | Yes, one layer at a time |
| � (U+FFFD) | A decoder replaced bytes it could not read | No: re-read the original source |
| ? where a letter should be | An encoder replaced characters it could not represent | No |
é or \u00e9 shown literally | Escaped text that was never unescaped (Text Encoding and Unicode 7) | Yes: unescape at the right layer |
| Letters separated by invisible NULs | 16-bit text read as 8-bit (Text Encoding and Unicode 4) | Yes: decode as UTF-16 |
| Empty boxes only | Characters are right but the font lacks glyphs | Nothing to repair: change font |
Steps 3 to 5: A Toolkit and a Capstone
For the reversible case, "UTF-8 read as Windows-1252", the repair is to encode the garbled text back to bytes with the wrong codec, then decode those bytes as UTF-8. Real data complicates this in two ways: some bytes are undefined in Windows-1252 (so many tools turn them into the matching control character, which we copy), and damage may have happened more than once. The program below also contains a guard: it only accepts a repair if it reduces the number of tell-tale marker sequences, so clean text, and legitimate text such as the Portuguese city name in capitals, is left alone.
The capstone follows the method. Step 1: the repr of the original shows an invisible U+FEFF (a BOM) at the front. Step 2: the first damaged copy shows "" and "ë", the second shows the same marks twice over (""): two layers. Step 3: every byte survived, so the damage is reversible.
Step 4: peel reports 2 layers and recovers the original exactly (True), which means the fault happened twice, that is, at two boundaries. Step 5 would be to find those two boundaries and set the encoding correctly there; step 6, to add a test containing a BOM, an accent and an euro sign.
The final line strips the BOM, which should be removed once its job is done. As a check on the guard, "SÃO PAULO" was left untouched because reversing it would not produce valid UTF-8.
Finding the Boundary
Step 4 is a search: run the text through the stages in order and report the first stage whose output differs from the original. Here each stage is a real round trip (file, JSON, database, export), and in each run we inject a fault at one stage:
Each injected fault is found at its own stage. Without a fault, no stage is reported. The last row is the irreversible case, with question marks: it can be found, but not repaired. The method scales: in a real system the "stages" are captures and log lines taken at each boundary, and the comparison is done by eye against the hex dump from step 1.
peel only to rescue existing damaged data, then fix the boundary that caused it. Also never repair blindly: keep a copy of the original, and review a sample before bulk-fixing.
What the Course Gave You
| Part | The idea to keep |
|---|---|
| Text Encoding and Unicode 1 | Bytes, code points and what a reader sees are three different counts |
| Text Encoding and Unicode 2 | UTF-8 is a small, strict bit pattern; decoders must refuse overlong forms and surrogates |
| Text Encoding and Unicode 3 | Legacy encodings assign the upper half of a byte differently; "no error" proves nothing |
| Text Encoding and Unicode 4 | UTF-16 uses surrogate pairs; byte order marks tell the order but are not always present |
| Text Encoding and Unicode 5 | Normalize (NFC) before comparing; use NFKC only for matching |
| Text Encoding and Unicode 6 | Count, cut and reverse by grapheme cluster when the audience is a person |
| Text Encoding and Unicode 7 | Say the encoding at every boundary: files, headers, pages |
| Text Encoding and Unicode 8 | Databases store what they are given; check character set, connection and collation |
| Text Encoding and Unicode 9 | Decode strictly, normalize, then validate; make invisible characters visible |
Exercises
Write report(s) that checks a string for the symptoms in the field guide (replacement characters, a question mark between letters, BOM bytes shown as text, mojibake markers, numeric entities, literal backslash-u escapes, NUL characters) and returns a list of findings. Try it on eight samples, including a clean one.
Using the toolkit from this chapter, damage a clean string by zero, one, two and three layers of "UTF-8 read as Windows-1252" and check that peel recovers the original each time. Then show two irreversible cases (ASCII replacement with question marks, and UTF-8 decoded as ASCII with replacement) and what peel does with them.
Extend the boundary search: build a four-stage pipeline (file, JSON, database, export) with correct round-trip stages, then inject a fault at each stage in turn (a wrong-encoding read at the file, the database, and a lossy ASCII export) and confirm first_bad_stage names the right stage each time. What does the output of the lossy case tell you about repair?
Chapter 10 Quick Reference
- Method: look at bytes, recognise the pattern, decide reversible or lost, find the boundary, fix at the source, prevent
ascii(s)ands.encode("utf-8").hex(" ")show what is really there- é, ’,  mean UTF-8 read as Windows-1252/Latin-1; U+FFFD and ? mean the data is already lost
- Reverse one layer:
s.encode(wrong).decode("utf-8"), copying the tolerant handling of the undefined bytes; guard against false repairs - A faulty boundary is found by comparing the text after each stage with the original
- Repair functions rescue existing data; the real fix is to declare the right encoding where the fault happens
- Test with non-ASCII data: accents, emoji, a decomposed accent, a BOM