Skip to content

description: "Strings & Formatting — Write a function slugify(title: str) -> str that converts 'Hello, World! ' into 'hello-world' — lowercase, punctuation stripped…"---

06 · Strings & Formatting

String basics

s = "Hello, World!"

print(s.lower())        # hello, world!
print(s.upper())        # HELLO, WORLD!
print(s.replace("World", "Python"))  # Hello, Python!
print(s.split(", "))    # ['Hello', 'World!']
print(" ".join(["a", "b", "c"]))     # a b c
print(s.strip())        # removes leading/trailing whitespace
print(len(s))           # 13
print(s[7:12])          # World

f-strings (preferred formatting method)

name = "Ada"
age = 30
pi = 3.14159265

print(f"{name} is {age} years old")
print(f"pi rounded: {pi:.2f}")        # pi rounded: 3.14
print(f"{age:>5}")                     # right-align in width 5
print(f"{'x' * 3}")                    # expressions work inside f-strings
print(f"{name=}")                      # debug shorthand: name='Ada'

Multi-line strings & raw strings

paragraph = """
This spans
multiple lines.
"""

path = r"C:\Users\ada\data"   # raw string: backslashes aren't escape chars

Common string checks

"42".isdigit()        # True
"hello".startswith("he")  # True
"hello".endswith("lo")    # True
"  ".isspace()         # True
"Hello" in "Hello, World!"  # True (substring test)

Immutability

Strings can't be modified in place — every "modification" returns a new string:

s = "hello"
s.upper()      # returns "HELLO" but doesn't change s
s = s.upper()  # you must reassign to keep the result

How It Actually Works

A str is an immutable array of Unicode code points, and CPython stores it compactly using a "flexible string representation" (PEP 393): it picks the smallest element width the string needs — 1 byte per character if every code point fits in Latin-1, 2 bytes if it fits in the Basic Multilingual Plane, 4 bytes otherwise. So a plain ASCII string uses 1 byte per character plus a header; adding one emoji forces the whole string to 4-byte storage. The object also caches its length and hash.

Immutability is enforced because there is simply no bytecode or method that writes into an existing string's buffer. s.upper() allocates a new string object and returns it; s still points at the old one until you reassign. This is what lets CPython safely intern strings — identifier-like literals are stored once in a global table and reused, so a = "hello"; b = "hello" often gives a is b.

f-strings are resolved at compile time, not by parsing at runtime. The compiler splits f"{name} is {age}" into literal chunks and expression chunks, compiles each {...} as real bytecode, and emits instructions to evaluate them and join the pieces (via FORMAT_VALUE/BUILD_STRING, or the dedicated FORMAT_SIMPLE/FORMAT_WITH_SPEC opcodes in 3.12+). The format spec after : (like .2f) is handed to the value's __format__ method. Because it's compiled, an f-string has no dictionary lookup overhead the way str.format or % do, and syntax errors inside {} are caught when the file is compiled.

🔀 See this in another language

Exercise

Write a function slugify(title: str) -> str that converts "Hello, World! " into "hello-world" — lowercase, punctuation stripped, spaces replaced with hyphens.