word01word = "café"2word[0]3word[1:3]4len(word)5word[0] = "C"6new = word + "s"7up = word.upper()8word < "cat"9out = "+".join(word)1230123456TypeError
Code card: the program; the highlighted line is the one runningMemory boxes: one per character, index on topCode-point tags: the number Unicode gives each characterFront row: free memory where each new string is builtEach step highlights one line of the program. A token hops along the step's arrow: out of the word row when a line reads characters, and from a line into the front row when it builds a new string. The boxes' characters crossfade in when the token lands. Line 5 tries to change a character and fails with a TypeError, because strings are immutable; +, upper() and join() therefore build new strings, and word stays 'café' throughout. During the comparison the tags show each character's code point. The drawing gives each character one box. Inside, CPython stores a whole string as one block using 1, 2 or 4 bytes per code point, chosen by its widest character (PEP 393); "café" fits in 1 byte each.

A row of characters

  1. A string is text: characters kept in order. Line 1 stores "café" as four characters, c, a, f and é. Each one has a numbered place, its index, counting from 0, just like a list.

Always a new string

Compare and join

Try it in the Python prompt

;; step 1/9 · line 1: word = "café"
>>> word = "café"
>>> word
'café'
>>> type(word)
<class 'str'>
>>> list(word)
['c', 'a', 'f', 'é']

The drawing gives each character one box. Inside, CPython stores a whole string as one block using 1, 2 or 4 bytes per code point, chosen by its widest character (PEP 393); "café" fits in 1 byte each. The Python docs warn that building a string by repeated concatenation has a quadratic cost: doubling the text roughly quadruples the work. CPython can sometimes extend a string in place, but code should not rely on it.