Strings are immutable sequences of Unicode
Python's str is a sequence of Unicode code points — not bytes, not ASCII characters. len("안녕") is 2, not 6 (which is the UTF-8 byte length). This matters everywhere — slicing, comparison, encoding boundaries.
Strings are also immutable (per lesson 4). You cannot modify a string in place; every operation that looks like modification (concatenation, replacement, case change) creates a new string. For one-off operations this is invisible. For tight loops that build large strings character by character — it matters, and the idiom is "".join(parts), not repeated +=.
f-strings — the modern way to build strings
Since Python 3.6, f-strings (formatted string literals) are the default for string interpolation. Prefix the string with f and use {expression} inside — the expression is evaluated and embedded.
f-strings can hold any expression, not just variable names. f"{2 + 2}" is "4". f"{user.name.upper()}" works. f"{items[0]}" works. The expression is evaluated at the moment the f-string is constructed.
Python 3.12+ removed most syntactic restrictions on f-strings — you can now nest the same quote character inside, use multi-line expressions, embed comments. Earlier versions required workarounds for these.
The format spec mini-language
Inside an f-string's braces, after a colon, lives Python's format specification mini-language. This is where most people stop learning f-strings, and that's a shame — the format spec gives you precise control over width, alignment, padding, precision, and number formatting.
Common patterns — {x:.2f} formats float to 2 decimals. {x:>10} right-aligns in width 10. {x:,} adds thousands separators. {x:%} formats as percentage. {x:08b} formats as 8-digit binary with leading zeros. There's a full DSL here; the official spec is one page and worth reading once.
logger.info(f"[{request_id}] brain={brain} took {elapsed:.2f}s"). The :.2f on elapsed means I never log 0.4837192847 when I want 0.48. Format spec is the difference between sloppy logs and clean ones.
String methods — there are a lot, and you'll use a few constantly
The methods that show up in real code, in order of frequency:
.strip(),.lstrip(),.rstrip()— remove whitespace (or specified chars) from edges.split(sep),.rsplit(sep, maxsplit)— split into list. Default splits on any whitespace..join(iterable)— the inverse of split.", ".join(["a", "b"])gives"a, b"..startswith(prefix),.endswith(suffix)— prefer these over slicing for prefix/suffix checks..replace(old, new)— substring replacement (no regex; for that, use theremodule)..lower(),.upper(),.title(),.casefold()— case manipulation.casefoldis the right choice for case-insensitive comparison (handles edge cases like German ß)..find(sub),.index(sub)— locate substring.findreturns -1 if missing;indexraises..count(sub)— count non-overlapping occurrences..isdigit(),.isalpha(),.isalnum(),.isspace()— character class checks.
Slicing — substrings without a method call
Strings, like lists, support slicing with [start:stop:step]. s[0] is the first character. s[-1] is the last. s[1:5] is characters 1 through 4 (stop is exclusive). s[::-1] reverses the string.
This is one of those areas where Python's expressiveness shows. Reverse a string — s[::-1]. Get the last 5 chars — s[-5:]. Drop the first 2 — s[2:]. Drop the last 3 — s[:-3]. Every other character — s[::2].
Escape sequences and raw strings
Common escapes — \n newline, \t tab, \\ literal backslash, \" quote inside double-quoted string. Unicode escapes — \u00e9 for é, \N{GREEK SMALL LETTER ALPHA} for α.
For strings full of backslashes (regex patterns, Windows paths), use raw strings — prefix with r. r"\n" is two characters (backslash, n), not one (newline). Regex patterns and Windows paths almost always want raw strings.
Triple quotes for multi-line strings
"""...""" or '''...''' spans multiple lines and preserves literal newlines. Triple-quoted strings at the top of a function become docstrings — accessible via help(func) and func.__doc__. This is how Python documents code; we'll use docstrings throughout the rest of the quest.
Bottom line
f-strings are the modern interpolation. The format spec mini-language is worth learning. Slicing replaces a lot of method calls. Raw strings exist for backslash-heavy contexts. Strings are sequences of Unicode code points, not bytes — that distinction matters when you cross encoding boundaries (file I/O, network, database).
str.format works but is older. %-style is even older. f-strings read top-to-bottom (variable values inline with the surrounding text), execute fast, and integrate with the format spec. The other styles are for reading existing code and very specific niches; for new code, f-string is the answer.