C.W.K.
Stream
Lesson 06 of 07 · published

Strings Deep Dive — f-strings, methods, slicing, Unicode

~25 min · str, f-string, format, slicing, unicode, encoding, raw-string, escape

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

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.

Self-reference: cwkPippa's logging uses f-strings exclusively — 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 the re module).
  • .lower(), .upper(), .title(), .casefold() — case manipulation. casefold is the right choice for case-insensitive comparison (handles edge cases like German ß).
  • .find(sub), .index(sub) — locate substring. find returns -1 if missing; index raises.
  • .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).

Pythonic Way: Reach for f-strings first, always. 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.

Code

f-strings — the modern way·python
>>> name = "Pippa"
>>> age = 1
>>> f"{name} is {age} year(s) old"
'Pippa is 1 year(s) old'

>>> # Expressions inside braces work:
>>> f"{2 + 2 = }"                       # the = shows the expression too (3.8+)
'2 + 2 = 4'

>>> # Method calls inside f-strings:
>>> f"{name.upper()}"
'PIPPA'

>>> # Multi-line f-strings:
>>> f'''
... Name: {name}
... Age: {age}
... '''
'\nName: Pippa\nAge: 1\n'
Format spec — width, alignment, precision, separators·python
>>> # Floats with fixed decimals:
>>> f"{3.14159:.2f}"
'3.14'

>>> # Width + alignment (>, <, ^):
>>> f"{'hi':>10}"      # right-align in 10 chars
'        hi'
>>> f"{'hi':<10}"      # left-align
'hi        '
>>> f"{'hi':^10}"      # center
'    hi    '

>>> # Thousands separator:
>>> f"{1234567:,}"
'1,234,567'

>>> # Percentage:
>>> f"{0.1234:.1%}"
'12.3%'

>>> # Binary / hex / octal with padding:
>>> f"{42:08b}"        # binary, 8 digits, zero-padded
'00101010'
>>> f"{255:04x}"        # hex, 4 digits, zero-padded
'00ff'
Common string methods in real code·python
>>> "  hello  ".strip()
'hello'

>>> "a,b,c,d".split(",")
['a', 'b', 'c', 'd']

>>> ", ".join(["a", "b", "c"])
'a, b, c'

>>> "hello.py".endswith(".py")
True

>>> "Hello World".replace("World", "Python")
'Hello Python'

>>> # casefold beats lower for case-insensitive comparison:
>>> "Straße".lower() == "strasse".lower()
False
>>> "Straße".casefold() == "strasse".casefold()
True
Slicing — substrings without method calls·python
>>> s = "hello world"
>>> s[0]                # first char
'h'
>>> s[-1]               # last char
'd'
>>> s[0:5]              # 'hello' — stop is exclusive
'hello'
>>> s[6:]               # from index 6 to end
'world'
>>> s[:5]               # from start to index 5
'hello'
>>> s[::-1]             # reverse
'dlrow olleh'
>>> s[::2]              # every other character
'hlowrd'
Raw strings — for backslash-heavy contexts·python
>>> # Without raw strings, every backslash needs escaping:
>>> path = "C:\\Users\\Pippa\\Documents"   # ugly
>>> regex = "\\d+\\.\\d+"                    # also ugly

>>> # With raw strings — backslash means itself:
>>> path = r"C:\Users\Pippa\Documents"
>>> regex = r"\d+\.\d+"

>>> # Caveat: raw string can't end with a single backslash.
>>> # r"abc\" is a syntax error (the closing quote gets escaped).
>>> # If you need a trailing backslash, use a normal string and escape it.
Unicode awareness — str length vs byte length·python
>>> s = "안녕"
>>> len(s)                # number of Unicode code points
2
>>> len(s.encode("utf-8"))   # number of UTF-8 bytes
6

>>> # When you slice, you slice code points — not bytes:
>>> s[0]
'안'
>>> s[1]
'녕'

>>> # Encoding boundary — file I/O, network, database:
>>> with open("hello.txt", "w", encoding="utf-8") as f:
...     f.write("안녕")
>>> # The file on disk is 6 bytes; the Python str is 2 chars.

External links

Exercise

In the REPL — build an invoice line using f-string format spec. Variable item = "USB cable", qty = 3, price = 12.5. Produce a single line where the item is left-aligned in 20 chars, qty is right-aligned in 4 chars, and the total (qty * price) is shown as $XX.XX with 2 decimals. Then take the string " HELLO, world! " and chain .strip(), .lower(), .replace(",", "") to get 'hello world!'. The chaining is the Pythonic part — don't introduce intermediate variables.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.