Chapters
Python114 chapters

StringsChapter 23 of 114

Text and Unicode

What a character really is, and the difference between text and bytes.

Text is not bytes

A str holds characters. A bytes holds numbers between 0 and 255. Files and networks carry bytes, so text has to be encoded on the way out and decoded on the way back:

Python
text = "Müller"
data = text.encode("utf-8")

print(type(text).__name__, type(data).__name__)
print(len(text), len(data))
print(data)
print(data.decode("utf-8"))

Output

str bytes
6 7
b'M\xc3\xbcller'
Müller

Six characters, seven bytes. The ü takes two. That gap is the whole subject.

UTF-8 is the answer

It can represent every character, and plain ASCII text encodes to exactly the same bytes it always did:

Python
print("abc".encode("utf-8"))
print("abc".encode("ascii"))
print("€".encode("utf-8"))

Output

b'abc'
b'abc'
b'\xe2\x82\xac'

Name it explicitly whenever you open a file. The default depends on the platform, which is how the same code produces different results on two machines.

Decoding with the wrong encoding

The bytes are fine; the interpretation is not. This is where mojibake comes from:

Python
data = "Müller".encode("utf-8")

print(data.decode("utf-8"))
print(data.decode("latin-1"))

try:
    data.decode("ascii")
except UnicodeDecodeError as problem:
    print("UnicodeDecodeError at byte", problem.start)

Output

Müller
Müller
UnicodeDecodeError at byte 1

ü on a web page always means this: UTF-8 bytes read as Latin-1.

Code points

Each character has a number. ord() gives it, chr() goes back:

Python
print(ord("A"), ord("ü"), ord("€"))
print(chr(65), chr(252))
print("ü", "\N{EURO SIGN}")

Output

65 252 8364
A ü
ü €

Length is characters, not width

Python
print(len("ü"))
print(len("👍"))
print(len("👨‍👩‍👦"))

Output

1
1
5

That family emoji is five code points joined by invisible characters. Nothing is broken — "how many characters" is simply a harder question than it looks, and len() answers a precise version of it.

The same letter, written two ways

é can be one code point or two: e plus a combining accent. They look identical and are not equal:

Python
import unicodedata

one = "é"        # e-acute as a single code point
two = "é"       # a plain e, then a combining accent

print([hex(ord(c)) for c in one])
print([hex(ord(c)) for c in two])
print(one == two)
print(len(one), len(two))
print(unicodedata.normalize("NFC", two) == one)

Output

['0xe9']
['0x65', '0x301']
False
1 2
True

Both render as e-acute and neither is wrong. Printing the code points is the only way to see the difference. Normalise with unicodedata.normalize before comparing text that came from different sources. This is why a search box sometimes fails to find a name that is visibly right there.

Handling bad bytes

When you cannot fix the source, choose how to fail:

Python
data = b"caf\xff"

print(data.decode("utf-8", errors="replace"))
print(data.decode("utf-8", errors="ignore"))

Output

caf�
caf

replace keeps a marker so you can see something was lost. ignore throws it away silently, which is usually the wrong trade.

Test yourself

2 questions

Why is len("Müller") 6 but len("Müller".encode("utf-8")) 7?

Show the answer

The umlaut takes two bytes in UTF-8 — A str counts characters and bytes counts bytes. Anything outside ASCII takes more than one byte.

You see ü where ü should be. What happened?

Show the answer

UTF-8 bytes were decoded as Latin-1 — The bytes are fine; the interpretation is wrong. It is the single most common encoding bug.

Next chapter

Lists

An ordered, changeable run of items - the collection you will use most.