Legacy Encodings
Text Encoding and Unicode
Course 1 · Chapter 3 · Legacy Encodings
UTF-8 did not arrive on an empty field. For decades, text meant one byte per character, and every region and vendor filled the spare half of the byte differently. Those older encodings are still around in old files, databases, email and web pages, and they are the source of most garbled text you will meet. This chapter explains the main ones: ASCII, Latin-1, and Windows-1252.
ASCII: Seven Bits
ASCII was first published in 1963 as ASA X3.4-1963 (Wikipedia). It uses 7 bits and defines 128 codes, numbered 0 to 127: 33 control codes (the first 32 plus DEL at 127) and 95 printable characters. Space is 32 (0x20), the digit 0 is 48 (0x30),
A is 65 (0x41) and a is 97 (0x61).
The designers placed each lowercase letter exactly 0x20 above its capital, so the two differ in a single bit (bit 5). That lets you change case with one XOR. ASCII also cannot represent anything outside basic English text, and Python refuses rather than guesses:
The last three lines show Python's error handlers: replace silently swaps in a question mark (data is lost), backslashreplace keeps a readable escape, and xmlcharrefreplace writes an HTML numeric entity. The default, strict, raises an error, which is usually what you want.
(Output is plain bytes, so the entity é shown above is literally the five characters é in the byte string.)
The Eighth Bit: Everyone Filled It Differently
A byte has 8 bits, so ASCII leaves 128 spare values (128 to 255). Each region and vendor filled them with what its users needed, producing many incompatible code pages. The same byte therefore meant different things depending on which table the reader used.
ISO-8859-1, called Latin-1, covers Western European languages. Its printable upper half runs from 0xA0 to 0xFF. Its special property: every byte value is equal to the Unicode code point with the same number (Unicode's first 256 code points were chosen to match), which Python confirms below. Here is the single byte 0xE9 and the byte 0xA4 under six code pages:
One byte, six tables, four different letters for 0xE9: é, ι (Greek iota), Θ and И. Nothing in the byte tells you which table was meant; that information must travel with the data (a later chapter covers how files and web pages declare it) or be guessed. The name cp437 is the original IBM PC character set (check),
koi8-r is a Russian encoding, and iso8859-15 is a revision of Latin-1 that includes the euro sign.
That iso8859-7 (Greek) shows a euro at 0xA4 reflects a later revision of that standard in Python's table (check).
Windows-1252: The Latin-1 Lookalike
ISO-8859-1 reserves 0x80 to 0x9F for C1 control codes, which are almost never used. It also has no euro sign (the standard dates from 1987, before the euro was presented in 1996, Wikipedia), no curly quotes and no dashes.
Microsoft's Windows-1252 matches Latin-1 everywhere except in that block, where it puts printable characters: euro sign, curly quotes, dashes, the trademark sign. It is so common that HTML5 browsers treat the label "iso-8859-1" as Windows-1252 (Wikipedia). Python shows the difference exactly:
All 32 bytes of the C1 block differ, and only there. Five of them (0x81, 0x8D, 0x8F, 0x90, 0x9D) are undefined in Windows-1252: Wikipedia says the same five are unused, and notes that some Windows APIs map them to the C1 controls anyway, a behaviour Python's strict codec does not copy.
The euro sign is byte 0x80 in Windows-1252 but 0xA4 in ISO-8859-15. This is why a text pasted from a word processor, full of curly quotes, shows odd symbols if read as the wrong one of the two.
Why Mojibake Has a Shape
Last chapter ended with UTF-8 bytes read as Latin-1. The most common modern accident is the same mistake with Windows-1252 as the wrong reader. UTF-8 bytes for accented letters and curly punctuation start with 0xC3, 0xE2 and similar, which the old tables display as "Ã" and "â", followed by symbols.
Recognise that shape and you know the cause: UTF-8 text decoded as a legacy encoding.
The first half is a clean repair: encode the garbled text back with the same wrong encoding to recover the original bytes, then decode correctly. The second half shows where it breaks: the right double quote is the bytes E2 80 9D, and 9D is one of Windows-1252's undefined bytes,
so the strict decode fails. Latin-1, which defines all 256 bytes, never fails and can always be reversed. Whether a damaged copy can be fixed therefore depends on which wrong decoder was used, and on whether anything was replaced by ? or U+FFFD along the way (then the data is gone). Libraries exist that automate such repairs (check).
Exercises
Write flip_case(s) that swaps the case of ASCII letters using XOR with 0x20 and leaves everything else alone. Check it against str.swapcase for all 128 ASCII characters, then try it on "café" and say why the é is untouched.
Write compare(b) that decodes one byte as Latin-1 and as Windows-1252, returning "(undefined)" when the second fails. Print it for 0x80, 0x85, 0x92, 0x93, 0x99, 0x9D, 0xA3 and 0xE9, then count how many of the 256 byte values differ and confirm they all lie in 0x80 to 0x9F.
Write which_work(raw, names) returning the encodings in a list that decode the bytes without error. Try it on plain English, UTF-8 text with an accent, Latin-1 text with an accent, and a euro sign in Windows-1252. What does the result tell you about guessing an encoding from "no error"?
Chapter 3 Quick Reference
- ASCII: 7 bits, 128 codes (33 control, 95 printable); A is 65, a is 97, and case differs by one bit (
0x20) - Bytes 128 to 255 were assigned differently by each region and vendor: the same byte, different letters
- Latin-1 (ISO-8859-1): byte value equals Unicode code point for all 256 values; 0x80-0x9F are controls; no euro sign
- Windows-1252: Latin-1 except 0x80-0x9F, which hold printable characters (euro at 0x80, curly quotes); five bytes undefined
- HTML5 treats the "iso-8859-1" label as Windows-1252
- Mojibake like "é" or "’" usually means UTF-8 bytes read by a legacy decoder; reverse it by encoding with the wrong codec and decoding as UTF-8
- Latin-1 never raises a decode error, so "no error" does not prove an encoding is right
- Encode error handlers:
strict(default),replace,backslashreplace,xmlcharrefreplace
cp437, koi8-r and iso8859-15 refer to, the Greek euro revision, how Python's strict codec compares with Windows APIs beyond the Wikipedia remark, and the existence of automatic repair libraries are from general knowledge (check).
Sources: en.wikipedia.org/wiki/ASCII, en.wikipedia.org/wiki/ISO/IEC_8859-1, en.wikipedia.org/wiki/Windows-1252.