Module 1 15 min

Text as Numbers

ASCII, Unicode, and how every character you type is secretly a number.

You now know computers only store numbers. But you're reading text right now, and you'll spend this whole course feeding text to language models. So somewhere, there must be an agreement that says which number means which character. That agreement is the story of this lesson: ASCII, then Unicode.

Why does this exist?

A computer cannot store the letter A — only numbers. So humanity needed a shared lookup table: this number means this character. Without a universal agreement, text written on one machine turns to garbage on another. ASCII was the first widely adopted table; Unicode is the modern one that covers every human language, and it is the reason you can text an emoji from an iPhone to a laptop in Tokyo and it just works.

The problem

Early computing was chaos: every manufacturer invented its own character-to-number table. A file written on an IBM machine was gibberish on anything else. The fix, in 1963, was ASCII — the American Standard Code for Information Interchange: one shared table mapping 128 characters to the numbers 0–127.

Some anchors worth knowing:

'A' = 65    'B' = 66    ...   'Z' = 90
'a' = 97    'b' = 98    ...   'z' = 122
'0' = 48    '1' = 49    ...   '9' = 57
space = 32          newline = 10

Notice the design: the alphabet is contiguous, and lowercase is exactly uppercase + 32. That's not an accident — 32 is a single bit in binary, so early hardware could switch case by flipping one bit.

So the word Hi is stored as the numbers 72, 105 — which, from the last lesson, are stored as the bits 01001000 01101001.

Explore the table

Type into the explorer below and watch characters become numbers. Try your name. Try uppercase versus lowercase versions of the same letter and check the +32 pattern. Try a digit like 7 and notice its character code is not 7.

4 characters · 4 code points · 7 UTF-8 bytes

  • Character H, dec 72
    U+0048
    1001000
  • Character i, dec 105
    U+0069
    1101001
  • Character , dec 32
    U+0020
    100000
  • Character 👋, dec 128075
    U+1F44B
    11111010001001011
    needs multiple bytes

The problem with ASCII: it's American

128 slots covers English letters, digits, and punctuation — and nothing else. No é, no ñ, no Greek, Hindi, Arabic, Chinese, and certainly no 🎉. Through the 80s and 90s, every region bolted on its own incompatible extension for numbers 128–255, and cross-border text exchange became mojibake roulette (more on mojibake in the next lesson).

The fix was radical: one table for every character in every human writing system. Unicode.

  1. ASCII (1963)128 characters, numbers 0 to 127. English only. Fits in 7 bits.
  2. Extended chaos (1980s)
  3. Unicode (1991 onward)
  4. Backwards compatible

Code points

A Unicode number is called a code point, conventionally written as U+ followed by the value in hexadecimal (base 16 — a compact way to write binary that you'll see everywhere in computing):

'A'  = U+0041   (decimal 65 — same as ASCII)
'é'  = U+00E9   (decimal 233)
'न'  = U+0928   (decimal 2344 — Devanagari na)
'中' = U+4E2D   (decimal 20013)
'🎉' = U+1F389  (decimal 127881)

Every language exposes this mapping:

ord('A')        # 65
ord('🎉')       # 127881
chr(127881)     # '🎉'
"A".codePointAt(0)        // 65
"🎉".codePointAt(0)       // 127881
String.fromCodePoint(127881)  // '🎉'

A code point is not a byte

Resist the urge to think the number 127881 is what sits in memory for 🎉. A code point is an abstract ID in the Unicode table. How that ID gets packed into actual bytes is a separate step — an encoding, like UTF-8 — and it's the subject of the next lesson. Keeping "code point" and "byte" separate in your head will save you real debugging pain.

Why AI engineers care

Language models never see characters. Text is converted to numbers before anything else happens — and character encoding is step zero of that pipeline. Concretely:

  • Tokenizers start from this. Modern LLM tokenizers typically operate on the UTF-8 bytes of your text. Odd model behavior with emoji, accents, or non-English text often traces straight back to how those characters map to numbers.
  • String length is ambiguous. Is 🎉 one character? One code point? Four bytes? Two JavaScript "characters"? All of these answers are correct in different systems — and that ambiguity causes real bugs in context-length math and API payloads.

An anchor to memorize

It's worth permanently memorizing exactly one code: A = 65. From it you can derive the rest of the alphabet, the lowercase offset (+32), and it serves as a sanity check whenever you're staring at raw bytes.

Build it yourself

Implement a Caesar cipher — the ancient trick of shifting each letter forward in the alphabet — using character codes:

def caesar(text, shift):
    out = ""
    for ch in text:
        code = ord(ch)
        if 65 <= code <= 90:              # uppercase A-Z
            out += chr((code - 65 + shift) % 26 + 65)
        elif 97 <= code <= 122:           # lowercase a-z
            out += chr((code - 97 + shift) % 26 + 97)
        else:
            out += ch                     # leave everything else alone
    return out

print(caesar("Hello, World!", 3))   # Khoor, Zruog!

Verify that caesar(caesar(s, 3), -3) returns the original. Then try feeding it "café 🎉" — notice the non-ASCII characters pass through untouched, because the code checks numeric ranges. That's character-code thinking in action.

Summary

  • Computers store numbers, so text needs a shared character-to-number table.
  • ASCII (1963) maps 128 English characters to 0–127; A = 65, and lowercase = uppercase + 32.
  • Unicode extends this to every human language: each character gets a unique code point like U+1F389, and the first 128 match ASCII exactly.
  • A code point is an abstract ID, not bytes in memory — encoding (UTF-8) is the next step.
  • LLM tokenizers sit directly on top of this machinery; encoding quirks explain much odd model behavior with emoji and non-English text.