Skip to content

Code pages

Level: 101 → 201 · for anyone starting from zero

One line: A code page is ASCII plus a second opinion: every one of them keeps the first 128 numbers and disagrees with every other one about the second 128 — so a byte above 0x7F means nothing until somebody names a table, and the tables agree just enough that the wrong one reads like a typo.

What a code page is

ASCII used seven bits and settled 128 numbers. Files are made of eight-bit bytes, so 128 patterns were left over — and every country, vendor and operating system filled them in for itself. A code page is one of those fill-ins: a 256-entry table that agrees with ASCII below 0x80 and does as it pleases above.

That shared bottom half is the whole reason these files were interchangeable at all. Source code, CSV commas, HTTP headers, English prose — all of it lives below 0x80 and survives every table swap intact. It is also why the problem stayed invisible in the English-speaking world for a decade: nothing an American office typed ever went above the line.

The ones you will actually meet

Table Also called Its second half is for Where you meet it
Latin-1 ISO-8859-1 Western Europe: é ü ñ å ø HTTP's old default; the identity table (below)
Windows-1252 CP1252, "ANSI" Latin-1, with 32 slots reassigned to and smart quotes Almost every "almost right" file from a Windows desktop
Latin-2 ISO-8859-2 Central Europe: ą ę ł ś ż č ř Polish, Czech, Hungarian files, mostly pre-2005
Windows-1250 CP1250 Central Europe again — differently Windows in the same countries, incompatibly
CP437 "OEM", the DOS table Box-drawing characters, Greek letters, maths DOS, BIOS screens, old .txt art
CP850 DOS Latin-1 CP437 with more accented letters, fewer boxes DOS in Western Europe
KOI8-R Russian Cyrillic, ordered so that stripping the top bit leaves readable Latin Russian email and Usenet, and still some Linux boxes

Each name links its own table in Unicode's MAPPINGS archive ↗ — not a description of the code page but the 256 lines themselves, one byte to one code point, which is the form worth reading in a library about bytes.

Two things are deliberately not on this page. EBCDIC does not belong in the list at all — it disagrees with ASCII about the bottom half too, so it is not a code page in this sense; its story is in From the telegraph to Unicode. And the exact 32-byte difference between Windows-1252 and Latin-1 — the one that produces most real-world damage — has its own page in the real-data chapter.

SAP names these tables by number rather than by name. Those numbers are worth checking against your own system rather than quoting from any document, this one included; SAP code pages is where this library keeps them.

Latin-1 is the odd one

For every byte from 0 to 255, Latin-1 decodes it to the code point of the same number. 0xE9 is U+00E9. 0xFF is U+00FF. That is not a coincidence — Unicode's first 256 code points were taken from Latin-1 — and it has two consequences worth memorising.

First, decoding under Latin-1 can never fail, because there is no byte it has nothing to say about. That makes it the only lossless way to carry unknown bytes through a text type, and the worst possible tool for working out what a file is: it answers "fine" to everything. Second, it is the only table in the list that is a rule rather than a list. Every other code page is 128 numbers somebody has to ship — which the Rust example makes uncomfortably concrete.

The agreement is the dangerous part

The usual way to tell this story is "the tables disagree", and that is true but not the useful half. Measured over the 128 bytes above 0x7F, the disagreement is partial, and the amount varies wildly:

  • Latin-1 and Latin-2 agree on 71 of 128. A Polish file read as Latin-1 comes out mostly right, with a few wrong letters — which reads as a typing mistake, not a bug, so nobody investigates.
  • Latin-2 and Windows-1250 — two tables for the same languages — agree on only 81 of 128, so even getting the language right is not enough.
  • CP437 and KOI8-R agree with the whole ISO family on zero. That is the easy case: it is obviously garbage, so somebody fixes it.

The counts come out of section 3 of the Python run below, and they invert the intuition: the more similar two tables are, the longer their confusion survives. The worst outcome is not garbage — garbage gets reported. It is one wrong letter in a customer's surname, in a file that has been copied nine times since.

In Python

Verified output of code_pages_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. THE HALF EVERYBODY AGREES ON
------------------------------------------------------------------------
   printable ASCII, 0x20-0x7E: 95 of 95 bytes decode
   identically under all 7 tables.

     0x41 -> 'A' under every one of them
     0x37 -> '7' under every one of them
     0x2c -> ',' under every one of them

   That is the entire reason these files were interchangeable at all.
   Source code, CSV delimiters, HTTP headers and English prose live
   down here, which is why the problem stayed invisible in the
   English-speaking world for a decade.

2. ONE BYTE, ABOVE THE LINE
------------------------------------------------------------------------
   0xe9 -- 4 different characters:
     latin1   'é' U+00E9 LATIN SMALL LETTER E WITH ACUTE
     1252     'é' U+00E9 LATIN SMALL LETTER E WITH ACUTE
     8859-2   'é' U+00E9 LATIN SMALL LETTER E WITH ACUTE
     1250     'é' U+00E9 LATIN SMALL LETTER E WITH ACUTE
     437      'Θ' U+0398 GREEK CAPITAL LETTER THETA
     850      'Ú' U+00DA LATIN CAPITAL LETTER U WITH ACUTE
     koi8     'И' U+0418 CYRILLIC CAPITAL LETTER I

   0xb9 -- 5 different characters:
     latin1   '¹' U+00B9 SUPERSCRIPT ONE
     1252     '¹' U+00B9 SUPERSCRIPT ONE
     8859-2   'š' U+0161 LATIN SMALL LETTER S WITH CARON
     1250     'ą' U+0105 LATIN SMALL LETTER A WITH OGONEK
     437      '╣' U+2563 BOX DRAWINGS DOUBLE VERTICAL AND LEFT
     850      '╣' U+2563 BOX DRAWINGS DOUBLE VERTICAL AND LEFT
     koi8     '╧' U+2567 BOX DRAWINGS UP SINGLE AND HORIZONTAL DOUBLE

   Not one byte. Not a corrupted byte. The same byte, and the question
   'what character is this?' has no answer until somebody names a table.

3. HOW MUCH THE TABLES AGREE -- WHICH IS THE PART THAT HURTS
------------------------------------------------------------------------
   of the 128 bytes 0x80-0xFF, how many decode to the SAME character:

              latin1    1252  8859-2    1250     437     850    koi8
   latin1        128      96      71      49       0       0       0
   1252           96     123      39      70       0       0       0
   8859-2         71      39     128      81       0       0       0
   1250           49      70      81     123       0       0       0
   437             0       0       0       0     128      81       0
   850             0       0       0       0      81     128       0
   koi8            0       0       0       0       0       0     128

   Read the off-diagonal numbers. Latin-1 and Latin-2 agree on 71 of
   128 -- so a Polish file read as Latin-1 comes out mostly right, with
   a handful of wrong letters, which reads as a typo rather than a bug.
   The DOS and Cyrillic tables agree with the ISO family on ZERO, and
   that is the easy case: it is obviously garbage and gets fixed.

4. LATIN-1 IS THE ONE THAT IS AN IDENTITY
------------------------------------------------------------------------
   for every byte 0-255, latin_1 decodes it to code point b: True
     0xe9 -> U+00E9
     0xff -> U+00FF

     latin1   defines 256 of 256 byte values
     1252     defines 251 of 256 byte values
     8859-2   defines 256 of 256 byte values
     1250     defines 251 of 256 byte values
     437      defines 256 of 256 byte values
     850      defines 256 of 256 byte values
     koi8     defines 256 of 256 byte values

   Latin-1's first 256 code points ARE Unicode's first 256 -- not a
   coincidence, it is where Unicode took them from. Two consequences:
   decoding under Latin-1 can never fail, and Latin-1 is therefore the
   only lossless way to carry unknown bytes through a text type.

5. A POLISH FILE FROM 2005
------------------------------------------------------------------------
   'Łódź'.encode(latin_1   ) raises: 'Ł' is not in this table
   'Łódź'.encode(cp1250    ) = a3 f3 64 9f
   'Łódź'.encode(iso8859_2 ) = a3 f3 64 bc

   Two tables can both write it, and they do not write it the same way.
     written cp1250     a3 f3 64 9f
       read as cp1250     'Łódź'   <- correct
       read as iso8859_2  'Łód\x9f'
       read as latin_1    '£ód\x9f'
       read as cp437      'ú≤dƒ'
     written iso8859_2  a3 f3 64 bc
       read as cp1250     'ŁódĽ'
       read as iso8859_2  'Łódź'   <- correct
       read as latin_1    '£ód¼'
       read as cp437      'ú≤d╝'

   The two Central European readings are the cruel ones: three letters
   right and the fourth quietly wrong -- Ľ for ź, or a control character
   that prints as nothing at all. Nobody files a bug for that. They
   retype the word and move on, and the file goes on being wrong.
   The cp437 reading is the lucky one: it is obviously broken.

6. WHAT NONE OF THEM COULD DO
------------------------------------------------------------------------
   reachable by ONE 8-bit table                256  at the absolute most
   reachable by all 7 of them together        478
   code points Unicode has room for      1,114,112

   And you may only pick one table per file. That is not an argument
   about tidiness -- a Kraków office and an Athens office could not put
   both addresses in one file, because there was no byte to write.

In the terminal

iconv -f <table> is you naming the rulebook out loud. Nothing on the pipe knows it, checks it, or remembers it — which section 4 makes the point of.

Verified output of code_pages_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

1. ONE BYTE ABOVE 0x7F, UNDER FIVE TABLES
------------------------------------------------------------------------

   the byte e9, and what each rulebook says it is:
     table        UTF-8 out   character
     ISO-8859-1   c3a9  é
     ISO-8859-2   c3a9  é
     CP437        ce98  Θ
     CP850        c39a  Ú
     KOI8-R       d098  И

   Five tables, four answers. Nothing about the byte changed.

2. THE SAME FIVE TABLES, BELOW 0x7F
------------------------------------------------------------------------

   the byte 41:
     ISO-8859-1   41  A
     ISO-8859-2   41  A
     CP437        41  A
     CP850        41  A
     KOI8-R       41  A

   41 is A in all of them, and 41 on the way out too. Every code page
   keeps the 1963 agreement for the bottom half; that is what makes
   them code PAGES rather than unrelated tables.

3. ONE POLISH WORD, TWO TABLES THAT CAN BOTH WRITE IT
------------------------------------------------------------------------

   starting from UTF-8:
     00000000: c581 c3b3 64c5 ba                        ....d..
   $ iconv -f UTF-8 -t ISO-8859-2
     00000000: a3f3 64bc                                ..d.
   $ iconv -f UTF-8 -t CP1250
     00000000: a3f3 649f                                ..d.

   Same word, same length, different bytes -- they agree on Ł and ó and
   part company on ź. Two files, four bytes each, and no way to tell
   them apart except by being told.

4. SO ASK THE PIPE WHICH ONE IT HAS
------------------------------------------------------------------------

     bytes: a3f364bc
     table: 

   There is no second line. `file`, `xxd` and every other tool can
   show you the bytes and none of them can tell you the table, because
   it was never written down anywhere. Somebody has to say.

In Rust

Rust's std has exactly one decoder, for UTF-8, and no code pages at all — so writing one here shows what a code page is with the romance stripped off. Latin-1 is a function with no data in it; Windows-1250 is 128 literal numbers. (Those numbers were typed by hand and then machine-diffed entry by entry against Python's own cp1250 codec — the header comment carries the command. The 49 this program counts is arrived at independently of the 49 in the Python matrix above, which is the nearest thing to a proof either of them is right.)

Verified output of code_pages_rs.rs — regenerated by tools/run_examples.py, never hand-typed.

1. A CODE PAGE, AS A DATA STRUCTURE
------------------------------------------------------------------------
   fn latin1(b: u8) -> char { b as char }                 <- no data
   const CP1250_TOP: [char; 128] = [ ... ];              <- 128 numbers

   That asymmetry is the lesson. Latin-1 needs no table because it
   IS the identity: byte value equals code point, all the way to 255.
   Every other code page is a list somebody has to ship, and a list
   is a thing you can be given the wrong copy of.

2. THE SAME BYTES, THROUGH BOTH FUNCTIONS
------------------------------------------------------------------------
   bytes [a3, f3, 64, 9f]
     latin1  -> "£ód\u{9f}"
     cp1250  -> "Łódź"

   One of those is a Polish city and one is a pound sign and some
   punctuation. Both functions ran without complaint, because
   neither one has anything to complain about: a lookup cannot fail.

3. WHERE THE TWO TABLES AGREE, AND WHERE THEY DO NOT
------------------------------------------------------------------------
   of the 128 bytes 0x80-0xFF:
     the two tables agree on   49
     CP1250 defines nothing at 5

   twelve of the disagreements, from 0xA0 up where both tables
   have real letters rather than control codes:

     0xa1 ¡ vs ˇ 0xa2 ¢ vs ˘ 0xa3 £ vs Ł 0xa5 ¥ vs Ą
     0xaa ª vs Ş 0xaf ¯ vs Ż 0xb2 ² vs ˛ 0xb3 ³ vs ł
     0xb9 ¹ vs ą 0xba º vs ş 0xbc ¼ vs Ľ 0xbd ½ vs ˝

   Rust ships none of this. std has one decoder -- UTF-8 -- and for
   anything else you reach for a crate (`encoding_rs`), which is
   itself a statement: in 2026 a code page is a compatibility
   concern, not a way to write files.

If you are coming from Python or ABAP

Python. Every table on this page is a codec in your standard library, no install: bytes([0xE9]).decode('cp437') works today. That makes Python the fastest tool for identifying a mystery file — decode the same bytes under each candidate and read which one produces words. Two things to know. latin-1 is special-cased in exactly the way this page describes, so data.decode('latin-1') is the standard trick for carrying arbitrary bytes through a str and back unchanged. And codec names are forgiving — latin-1, latin_1, iso-8859-1 and 8859 all reach the same codec — which is convenient until you write one down and someone reads it as authoritative.

ABAP. The table is named at the boundary and nowhere else: OPEN DATASET … IN LEGACY TEXT MODE CODE PAGE … for a file, the destination's setting for an RFC, and cl_abap_codepage=>convert_from( ) when you have the bytes in an xstring already. SAP's numbers for these tables are a private naming scheme over the same public tables, and the mapping is worth verifying on your own system rather than trusting a list. The practical consequence is the one this page is about: a non-Unicode source system sending a Central European file, and a receiver told the wrong number, produces a file that mostly works — so the failure surfaces as a handful of odd surnames months later, not as a short dump. (Not machine-checked — CI cannot run ABAP.)

Try it

cd 02_Characters/code_pages/examples
python3 code_pages_py.py
bash code_pages_sh.sh
rustc --edition 2024 code_pages_rs.rs -o /tmp/cp && /tmp/cp

Without the machine: you are given a four-byte file, a3 f3 64 9f, and told only that it is one Polish word. You have Latin-1, Latin-2 and Windows-1250 available. How many of the three can you rule out, and what does that tell you about how much a file's bytes can ever say about themselves?

Practice

One byte, four tables. For each of 0x41, 0xE9 and 0x80, say what cp1252, Latin-1, cp1250 and mac-roman each make of it — and for each byte say whether the four agree, disagree, or one of them refuses.

Then the question the third byte settles: is cp1252 a superset of Latin-1?

Answers

Verified output of code_pages_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.

byte 0x41
   cp1252     U+0041   'A'
   latin-1    U+0041   'A'
   cp1250     U+0041   'A'
   mac-roman  U+0041   'A'

byte 0xe9
   cp1252     U+00E9   'é'
   latin-1    U+00E9   'é'
   cp1250     U+00E9   'é'
   mac-roman  U+00C8   'È'

byte 0x80
   cp1252     U+20AC   '€'
   latin-1    U+0080   (a control, nothing drawn)
   cp1250     U+20AC   '€'
   mac-roman  U+00C4   'Ä'

0x41 is A in all four, and in every code page there has ever been: the
first 128 are ASCII by agreement and that is the only thing they share.

0xE9 is e-acute in three of the four and a different letter in the
fourth. That is the dangerous shape -- not a refusal, not a mess, just a
plausible wrong letter, which is why the wrong table reads like a typo
rather than like a bug.

0x80 is the one that settles the 'is cp1252 a superset of Latin-1'
question, and the answer is NO. In cp1252 it is the euro sign; in
Latin-1 it is a C1 control character with nothing to draw. cp1252
REPLACED Latin-1's top control block, so the two tables disagree about
32 positions rather than one extending the other.

See also