Grade by Grade
A. Programming Concepts and Skills · Programming Basics: Data, Decisions and Loops

How Computers Store Text: ASCII and Unicode

A1.2About 65 minutes
Learning goals
  • I can explain that every character is stored as a number called a character code or code point.
  • I can use ord() and chr() to convert between characters and their codes.
  • I can describe the difference between ASCII and Unicode, and why Unicode is needed.
  • I can explain that UTF-8 stores a character in 1 to 4 bytes.

Words to know

character
A single letter, digit, punctuation mark, space, symbol or emoji.
character set
A list that gives every character a number (its code).
ASCII
American Standard Code for Information Interchange: a 7-bit character set with 128 codes (0 to 127) for English letters, digits, punctuation and control codes.
Unicode
The worldwide character set used by modern computers. It gives a code point to every character of almost every writing system, plus symbols and emoji.
code point
The number Unicode assigns to a character, written like U+0041 (hexadecimal) for A.
UTF-8
The most common way to store Unicode code points as bytes. It uses 1 byte for ASCII characters and 2 to 4 bytes for others.

Letters are numbers too

In the last lesson you saw that a computer stores only bits. So how does it store the word Bonjour, a French é, or a 🍁 emoji? The answer: every character is given a number, and the number is stored in binary.

The list that matches characters to numbers is called a character set. If two computers use the same character set, a message typed on one appears correctly on the other.

ASCII

ASCII was published in the 1960s. It uses 7 bits, so it has 2⁷ = 128 codes (0 to 127):

  • codes 0 to 31 (and 127) are invisible control codes, such as new line (10) and tab (9);
  • 32 is the space;
  • 48 to 57 are the digits 0 to 9;
  • 65 to 90 are the capitals A to Z;
  • 97 to 122 are the lower-case letters a to z.

Two patterns are worth remembering. The letters and digits are in order, and each lower-case letter is exactly 32 more than its capital (32 is 2⁵, a single bit). Also notice that the character "7" (code 55) is not the number 7.

digits048149250957…capitalsA65B66C67Z90…lower casea97b98c99z122…space = 32! = 33? = 63ord('a') - ord('A') = 97 - 65 = 32, and the same for every letter( … marks the codes skipped in this picture )
Some ASCII codes. Digits, capitals and lower-case letters each come in order, so ord("C") is 2 more than ord("A"), and every lower-case letter is exactly 32 more than its capital.

ord() and chr() in Python

  • ord("A") gives the code of a character: 65.
  • chr(65) gives the character with that code: "A".

They are opposites: chr(ord(c)) is always c. Because the codes are numbers, you can do arithmetic with them, then turn the result back into a character.

print(ord("A"), ord("a"), ord("0"), ord(" "))
print(chr(72) + chr(105))
print(chr(ord("C") + 3))
print(chr(ord("g") - 32))

Line 3 moves 3 letters forward from C. Line 4 turns a lower-case letter into a capital by subtracting 32. Output, line by line: 65 97 48 32 | Hi | F | G

Why "Zebra" comes before "apple"

When Python compares strings (Lesson 8), it compares character codes, one character at a time. "Z" is 90 and "a" is 97, so "Zebra" < "apple" is True: in code order, every capital comes before every lower-case letter. To sort words the way a dictionary does, programs convert them to the same case first.

Unicode: every writing system

ASCII has no é, no ç, no Chinese or Arabic characters, no syllabics and no emoji. In Canada alone, French needs accented letters, and First Nations and Inuit languages such as Cree and Inuktitut are often written with syllabics. Before Unicode, different countries used different, incompatible character sets, and text often turned into nonsense when it moved between computers.

Unicode fixes this with one huge character set for the whole world. It has well over 100 000 characters and is still growing. Each character has a code point, written U+ followed by hexadecimal digits:

  • A is U+0041 (65, the same as ASCII: Unicode's first 128 code points are ASCII);
  • é is U+00E9 (233);
  • the syllabics character ᐃ is U+1403 (5123). The Inuktitut word for the language, ᐃᓄᒃᑎᑐᑦ (Inuktitut), is 6 characters from Unicode's Unified Canadian Aboriginal Syllabics block, the official name of that block;
  • 🍁 (MAPLE LEAF) is U+1F341 (127 809).

Python 3 strings are Unicode, so ord("é") is 233 and len("ᐃᓄᒃᑎᑐᑦ") is 6.

Storing code points: UTF-8

A code point can be as large as about 1.1 million, which needs 21 bits. Storing every character in 4 bytes would waste space for English text. UTF-8, the encoding used by most web pages and files, uses a variable number of bytes:

  • 1 byte for code points 0 to 127 (so plain ASCII text is unchanged);
  • 2 bytes for most accented Latin letters, Greek, Cyrillic, Arabic and Hebrew;
  • 3 bytes for most other characters still in common use, including syllabics and Chinese;
  • 4 bytes for emoji and rarer characters.

In Python, "é".encode("utf-8") shows the actual bytes.

charcode pointUTF-8 bytes (in hex)AU+0041411 byteéU+00E9C3A92 bytesᐃU+1403E190833 bytes🍁U+1F341F09F8D814 bytes
In UTF-8, ASCII characters take 1 byte, and other characters take 2, 3 or 4 bytes. Shown: the letter A, the French é, the Canadian syllabics character ᐃ and the maple leaf emoji.
word = "café"
print(len(word))
print(len(word.encode("utf-8")))
print(len("🍁".encode("utf-8")))

café is 4 characters but 5 bytes in UTF-8, because é needs 2 bytes. The maple leaf needs 4 bytes. Output, line by line: 4 | 5 | 4

Worked example

Example 1: Encoding a word

Show how the string "Hi!" is stored in ASCII, in decimal and in 8-bit binary.

  1. Look up each code (or use ord): H = 72, i = 105, ! = 33.
  2. 72 = 64 + 8 → 01001000.
  3. 105 = 64 + 32 + 8 + 1 → 01101001.
  4. 33 = 32 + 1 → 00100001.
  5. The string takes 3 bytes, stored one after another.

Hi! is stored as 72, 105, 33, which is 01001000 01101001 00100001.

Worked example

Example 2: A one-letter Caesar shift

A Caesar cipher replaces each letter with the letter a fixed number of places later in the alphabet, wrapping from Z back to A. Write code that shifts the capital letter in letter by shift places.

  1. The capitals have codes 65 to 90. First turn the letter into a position 0 to 25: ord(letter) - 65.
  2. Add the shift and use % 26 so that 26 wraps to 0, 27 to 1, and so on.
  3. Turn the position back into a code by adding 65, then use chr.
  4. One line does it all: new_letter = chr((ord(letter) - 65 + shift) % 26 + 65).
  5. Test with letter = "X" and shift = 5: ord is 88, 88 − 65 = 23, 23 + 5 = 28, 28 % 26 = 2, 2 + 65 = 67, which is C.

chr((ord(letter) - 65 + shift) % 26 + 65); X shifted by 5 is C. (To encode a whole message you need a loop, which you will learn in Lesson 12.)

letter = "X"
shift = 5
new_letter = chr((ord(letter) - 65 + shift) % 26 + 65)
print(new_letter)

Example 2 as a program. Try other letters and shifts. Output: C

Just for fun

Unicode really does have a character named SNOWMAN (☃, U+2603), and a separate one named SNOWMAN WITHOUT SNOW (⛄, U+26C4). Canada approves.

Hands-on activity

Secret codes at the kitchen table

You need: paper, a pencil, a family member or friend, and Python.

  1. Write a short message (3 to 6 letters, capitals only) as a list of ASCII codes, for example 76 69 65 70 83. Use 65 for A and count up.
  2. Give the codes to your partner and have them decode it with the alphabet. Then swap roles.
  3. In Python, check your codes with ord() and decode your partner's message with chr().
  4. Now write the same message in Caesar code with a shift of 3 (A → D, X → A). Use the one-line formula from Example 2 to check two of the letters.
  5. Find two French words with accents (for example été and français). Use len(word) and len(word.encode("utf-8")). Why are the numbers different?

What to look for: 76 69 65 70 83 decodes to LEAFS. été is 3 characters but 5 bytes (each é takes 2 bytes); français is 8 characters but 9 bytes (ç takes 2 bytes).

Your turn

1

What is ord("B")? What is chr(100)?

2

Decode the ASCII codes 79 78 84 65 82 73 79.

3

What is ord("m") - ord("M")? Why?

4

Is "5" the same as 5 inside the computer? Explain using codes.

5

How many characters can ASCII represent, and why that number?

6

Give two reasons why Unicode replaced ASCII.

7

What does print(chr(ord("a") + 25)) display?

More practice: warm-up

1

What function gives the code of a character?

2

What function gives the character for a code?

3

What is the ASCII code for a space?

4

How is the code point of A written in Unicode notation?

5

How many bytes does UTF-8 use for the letter k?

More practice: on your own

1

Write ASCII codes in decimal for Go 2.

2

Write the binary for the character C (8 bits).

3

Write one line of Python that turns the capital letter in c into lower case using ord and chr.

4

What does print("Zoo" < "apple") display? Explain.

5

Shift the letter W forward by 6 places in a Caesar cipher (wrap around). Show the calculation.

Challenge

1

A text file contains 1000 characters of plain English plus 10 emoji, saved as UTF-8. How many bytes is it (ignore anything else)?

2

The digit characters "0" to "9" have codes 48 to 57. Write one expression that turns a digit character d into its integer value without using int().

3

Why is UTF-8 designed so that ASCII characters take only 1 byte, instead of giving every character 4 bytes?

Check your understanding

Quiz

1.

What is ord("a")?

2.

What is chr(67)?

3.

How many codes does ASCII have?

4.

Which statement about Unicode is true?

5.

How many bytes does the string "été" take in UTF-8?

6.

What does print(chr(ord("b") - 32)) show?

For parents and teachers

Big idea: this lesson completes expectation A1.2: how a computer uses ASCII and Unicode (with UTF-8) to represent text internally.

How to help: play the secret-code game together. If your family speaks or reads a language other than English, ask your teen to look up the code point of a character from it with ord().

Common difficulties:

  • Confusing the character "5" (code 53) with the number 5.
  • Forgetting that capitals and lower-case letters have different codes.
  • Mixing up characters and bytes: len() of a string counts characters, while the UTF-8 encoding may need more bytes.
  • When doing a Caesar shift by hand, forgetting to wrap from Z back to A.