Strings

Slice text, format it, and stop building strings the expensive way.

03of 24
≈ 13Min read
5Parts

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.

py
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[start:stop:step] — start included, stop excluded
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.

py
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.

MethodDoes
.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.

py
# 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

Practice