A string cannot be changed
Immutability is why a string can be a dictionary key, why it is safe to pass one anywhere, and why s.replace(...) on its own line does nothing at all — you have to keep the result.
name = "ada"
name[0] = "A" # TypeError: 'str' object does not support item assignment
name = name.title() # every string method returns a NEW string
print(name) # Ada
Slicing: the one notation to learn properly
A slice never raises for being out of range — "ab"[5:9] is "". Indexing does: "ab"[5] is an IndexError. The same notation works on lists, tuples and every other sequence, so learning it once pays off everywhere.
s = "performance"
s[0] # 'p' first
s[-1] # 'e' last, without knowing the length
s[0:4] # 'perf' up to but not including index 4
s[:4] # 'perf' a missing start means 0
s[4:] # 'ormance' a missing stop means the end
s[-4:] # 'ance' the last four
s[::2] # 'promne' every second character
s[::-1] # 'ecnamrofrep' reversed
len(s) # 11
f-strings
f-strings are the only formatting worth teaching now: they are faster than % and .format(), and the expression sits where it is read. f"{total=}" is the debugging form — it prints the name and the value together.
Anything inside the braces is an expression: f"{len(items)} items", f"{x if x else 'none'}". Since 3.12 the quotes inside can even match the outside ones.
name, total, ratio = "Ada", 1234.5678, 0.4213
f"{name} owes {total}" # 'Ada owes 1234.5678'
f"{total:.2f}" # '1234.57' two decimal places
f"{total:,.2f}" # '1,234.57' thousands separator
f"{ratio:.1%}" # '42.1%' as a percentage
f"{name:>10}|" # ' Ada|' right-aligned in 10
f"{name:*^11}|" # '****Ada****|' centred, padded with *
f"{255:#x}" # '0xff'
f"{total=}" # 'total=1234.5678' — prints the expression too
The methods that come up
.strip("abc") removes any of those characters from the ends, not that prefix — "aabbc".strip("ab") is "c". That is the bug removeprefix was added to prevent.
| Method | Does |
|---|---|
.strip() / .lstrip() / .rstrip() | Remove whitespace (or given characters) from the ends |
.split(sep) / .rsplit(sep, 1) | Text to list. With no argument it splits on any run of whitespace |
sep.join(parts) | List to text. This is how you build a string, not += in a loop |
.replace(a, b) | Every occurrence; pass a count to limit it |
.startswith() / .endswith() | Take a tuple: name.endswith((".jpg", ".png")) |
.lower() / .casefold() | casefold is the aggressive one, for comparing across languages |
.removeprefix() / .removesuffix() | Since 3.9 — what everybody used to write wrongly with strip |
in | "form" in "performance" — substring test, no method needed |
Building strings, and Unicode
A str is a sequence of Unicode code points. A bytes is a sequence of numbers 0–255. encode goes one way, decode the other, and Python refuses to mix them — which is the whole reason Python 3 exists.
# O(n²) in the worst case — a new string every time round
out = ""
for word in words:
out += word + " "
# one allocation
out = " ".join(words)
# text is Unicode; bytes are bytes
"café".encode("utf-8") # b'caf\xc3\xa9' — 5 bytes
len("café") # 4 characters
len("café".encode("utf-8")) # 5 bytes