Python Cheat Sheet Python 3.14 · language, interpreter and standard library · 1206 names indexed

Written for a reader who already knows what a hash table, a closure and a linearised MRO are, and wants the CPython specifics rather than the introduction. Pages 1–31 are the language as it is actually implemented: object and scope semantics, the container types and their costs, comprehensions and the iteration protocol, pattern matching, the descriptor and operator protocols, exception groups, the import machinery, the type system, async/await, the GIL and the free-threaded build — then four cards on the interpreter itself: object layout and the two-mechanism collector, the specialising adaptive interpreter and its bytecode, attribute lookup and C3, and binary-operator dispatch. The remaining pages index the built-ins, the operators, the data model and the standard library by category. Type in the filter box — or press / — to narrow the index; hover any name for its signature.

Where it lives: language core / built-in standard library new in 3.10–3.14 third-party (PyPI) deprecated / removed
Source: the Python Language Reference and Library Reference for 3.14 (docs.python.org, PSF) and the relevant PEPs; checked against CPython 3.14.6 on macOS. Hover any name for its signature and notes.

Python Programmer’s Guide

The language and the batteries — syntax, semantics, idioms and traps, Python 3.14

Running Python & Environments

3.14
python3 script.py a brun a file; argv = ['script.py','a','b']python3 -m module a brun an installed module as __main__ (pytest, http.server, venv)python3 -c "print(1+1)"run one statementpython3the REPL — since 3.13 with colour, block editing and F1 helppython3 -i script.pyrun, then drop into the REPL with its globalspython3 -m pdb script.pyrun under the debuggerecho "1+1" | python3 -read the program from stdin

Interpreter options worth knowing

FlagEffect
-O / -OOSet __debug__=False: strips assert and if __debug__; -OO also strips docstrings
-BDo not write __pycache__/*.pyc (also PYTHONDONTWRITEBYTECODE)
-uUnbuffered stdout/stderr — the fix when logs vanish in a pipe or container
-W errorTurn warnings into exceptions. -W error::DeprecationWarning to pick one
-X devDevelopment mode: extra checks, default warning filters, faulthandler on
-X importtimePrint per-import cost — the first thing to run on a slow CLI
-X gil=0Free-threaded build only: run without the GIL (3.13+, officially supported in 3.14)
-PDo not prepend the script's directory to sys.path (safer imports)
-IIsolated: -P -s plus ignore all PYTHON* environment variables
-v -q -E -sTrace imports · quiet banner · ignore env vars · ignore user site-packages

Environment variables

PYTHONPATHextra directories prepended to sys.path (colon-separated)PYTHONHOMEoverride the install prefix — almost always a mistakePYTHONBREAKPOINTwhat breakpoint() calls; =0 disables itPYTHONHASHSEED0 or an int to make str/bytes hashing reproduciblePYTHONWARNINGSsame syntax as -WPYTHONUTF8=1UTF-8 mode: force UTF-8 for stdio and open()PYTHONFAULTHANDLER=1dump a traceback on segfault

Virtual environments

python3 -m venv .venvcreate; --system-site-packages to inherit, --upgrade-deps to freshen pipsource .venv/bin/activatemacOS/Linux · .venv\Scripts\activate on Windowspython -m pip install -e .install this project, editablepython -m pip install -r requirements.txtthe classic lockfile-ish workflowpython -m pip freeze > requirements.txtpin exactly what is installeddeactivateleave the venv

Always python -m pip, never bare pip. The bare command may belong to a different interpreter; -m guarantees you install into the Python you are running.

uv — the fast modern front end

uv venvcreate .venv in millisecondsuv pip install ruffdrop-in pip replacementuv run script.pymake the env if needed, then runuv add requestsadd to pyproject.toml and lock (uv.lock)uv syncmake the env match the lock exactlyuv python install 3.14fetch an interpreter, no system installuv tool install ruffinstall a CLI tool in its own env (like pipx)

uv and ruff are third-party (Astral). pip and venv ship with CPython; ensurepip re-installs pip into a broken env.

A minimal pyproject.toml

[project]name = "mytool"version = "0.1.0"requires-python = ">=3.11"dependencies = ["requests>=2.32"][project.scripts]mytool = "mytool.cli:main"[build-system]requires = ["hatchling"]build-backend = "hatchling.build"

Read it back at runtime with tomllib (stdlib, read-only, binary mode) or importlib.metadata.version("mytool"). setup.py and distutils are gone — distutils was removed in 3.12.

Syntax & the Object Model

the mental model

Uniform reference semantics, no value types: every name is a pointer to a heap object carrying an identity, a type and a value, and assignment rebinds the pointer without copying. Functions, classes, modules and None are ordinary first-class objects; the only thing that varies is whether the referent is mutable. Layout and reclamation are in Object Layout & Memory.

The consequence that costs people time: parameter passing is call-by-object-reference, so a function can mutate its argument but cannot rebind the caller’s name.

a = [1, 2]; b = a # one list, two namesb.append(3); a # [1, 2, 3]b = b + [4] # rebinds b to a NEW list; a unchangedb += [5] # in-place for lists! a would change if b were still a

Mutable vs immutable

ImmutableMutable
int float complex boollist
str bytesdict set bytearray
tuple frozenset rangemost class instances
None Ellipsis NotImplementedarray, deque, memoryview targets

A tuple is immutable but can hold mutable things: t=([1],) then t[0].append(2) works. Hashability follows immutability — that tuple is not hashable.

Statement layout

if x: # colon opens a block do_this() # 4 spaces, consistently. NEVER mix tabs and spaces do_that()total = (a + b + # inside () [] {} newlines are free c + d)x = 1; y = 2 # legal, discourageddef f(): ... # ... is the Ellipsis object, the idiomatic stub body

Python 3 forbids mixing tabs and spaces for indentation outright (TabError). PEP 8: 4 spaces, 79-column lines (most projects use 88 — black/ruff format default).

Truthiness

An object is falsy if __bool__ returns False, else if __len__ returns 0, else it is truthy. Falsy built-ins: False None 0 0.0 0j Decimal(0) Fraction(0) "" b"" () [] {} set() range(0).

Never test containers with len(x) > 0 or compare to True. Write if items: and if flag:. But test for None explicitly with is None0, "" and [] are legitimate values that are also falsy.

Identity, equality and interning

x == yequal value — calls __eq__x is ythe same object — compares id()x is Nonethe only right way to test for None256 is 256True: small ints -5..256 are cached1000 is 1000CPython detail; never rely on it

CPython interns small ints and some strings. is on numbers or strings is a bug waiting for a bigger input. In 3.8+ you get a SyntaxWarning for is with a literal.

Comments, docstrings and the shebang

#!/usr/bin/env python3# a comment runs to end of linedef area(r): """Return the area of a circle of radius r.""" # __doc__ return 3.14159 * r ** 2

Names, Scope & Binding

LEGB

A name is looked up in four scopes, innermost first: Local → Enclosing function → Global (module) → Built-in. Class bodies are not in that chain for nested functions.

x = "global"def outer(): x = "enclosing" def inner(): print(x) # "enclosing" — found in E inner()

Writing to an outer name

global nameassignments in this function bind the module-level namenonlocal namebind the nearest enclosing function's name (never global)
Any assignment anywhere in a function makes the name local for the whole function. def f(): print(x); x = 1 raises UnboundLocalError on the print, not the assignment.

Closures and the late-binding trap

fs = [lambda: i for i in range(3)][f() for f in fs] # [2, 2, 2] — i is looked up when calledfs = [lambda i=i: i for i in range(3)][f() for f in fs] # [0, 1, 2] — default captures the value now

Closures capture the variable, not its value. Bind with a default argument, or functools.partial.

Comprehension scope

Comprehensions and generator expressions get their own scope, so the loop variable does not leak — unlike a for statement, whose variable survives the loop. The walrus operator deliberately assigns into the enclosing scope.

[i for i in range(3)]; i # NameErrorfor i in range(3): passi # 2 — for-loop names leakif (n := len(data)) > 10: ... # n survives, on purpose

del, and what it does not do

del nameunbind the name. The object dies only when nothing else refers to itdel d['k']remove a mapping key — calls __delitem__del lst[2:5]remove a slice

Inspecting scope

globals()the module's namespace dict — writable, and writes take effectlocals()a snapshot at function scope (3.13+: an independent snapshot, PEP 667)vars(obj)obj.__dict__; vars() with no argument == locals()dir(obj)every attribute name, including inheritedobj.__dict__instance attributes; absent if the class uses __slots__

Numbers & Arithmetic

int float Decimal
7 / 2 → 3.5true division — ALWAYS a float, even 4/27 // 2 → 3floor division; -7//2 is -4, it floors, not truncates7 % 3 → 1modulo; sign follows the DIVISOR: -7%3 is 2divmod(7,3) → (2,1)quotient and remainder in one call2 ** 10 → 1024power; right-associative, binds tighter than unary minus-2 ** 2 → -4reads as -(2**2)pow(3,100,7)modular exponentiation — fast, used in crypto

int

Arbitrary precision, no overflow. Literals may carry _ separators and a base prefix.

1_000_000underscores anywhere between digits0b1010 0o17 0xFFbinary, octal, hex literalsint("ff", 16)parse in a base; base 0 honours the prefixbin(10) oct(10) hex(255)back to a prefixed string(255).bit_length()8 — bits needed, sign excluded(255).bit_count()popcount (3.10+)int.from_bytes(b, "big")and (n).to_bytes(4, "big")

Converting an int of more than 4300 digits to str raises ValueError since 3.11 (a DoS fix). Raise it with sys.set_int_max_str_digits().

float — and why 0.1 + 0.2 != 0.3

IEEE-754 binary64: 53 bits of mantissa, ~15–17 significant decimal digits. Most decimal fractions are not representable.

0.1 + 0.2 → 0.30000000000000004not a bugmath.isclose(a, b)the right way to compare; rel_tol=1e-09 defaultround(2.675, 2) → 2.672.675 is really 2.67499...round(0.5) → 0banker's rounding: ties go to evenfloat("inf") float("nan")and math.inf / math.nannan == nan → Falseuse math.isnan()math.fsum(vals)exact summation; sum() drifts(0.1).as_integer_ratio()see the exact value it holds

Decimal — when the answer is money

from decimal import Decimal, getcontext, ROUND_HALF_UPDecimal("0.1") + Decimal("0.2") # Decimal('0.3') exactlygetcontext().prec = 28 # significant digitsDecimal("2.675").quantize(Decimal("0.01"), ROUND_HALF_UP) # 2.68
Always build a Decimal from a string. Decimal(0.1) faithfully copies the float's error.

Fraction and complex

Fraction(1,3) + Fraction(1,6)Fraction(1, 2) — exact rationalsFraction("0.25")→ 1/4; also Fraction(0.25) exactly3 + 4jcomplex literal; .real .imag .conjugate()abs(3+4j) → 5.0modulus; cmath for the transcendentals

Comparison chaining and bitwise

0 < x <= 10chained, x evaluated once, short-circuitsa & b a | b a ^ band, or, xor~a-(a+1) — two's complementa << n a >> nshift; >> floors for negatives

Strings & f-strings

str
'single' "double"identical; pick one and let the formatter enforce it"""triple"""spans lines, keeps the newlinesr"C:\new\table"raw: backslashes are literal. Use for EVERY regexb"bytes"bytes literal, ASCII onlyf"{name!r:>10}"formatted string literal"a" "b"adjacent literals concatenate at compile time → "ab""\N{GREEK SMALL LETTER PI}"character by Unicode name

Strings are immutable sequences of Unicode code points. Every mutation method returns a new string.

f-strings

name, n, x = "Ada", 42, 1234.5678f"{name} has {n} items" # Ada has 42 itemsf"{n=}" # n=42 — the debugging form (3.8+)f"{x:,.2f}" # 1,234.57f"{x:>12.3e}" # right-aligned scientificf"{n:08b}" # 00101010f"{value!r}" # repr(); !s str(); !a ascii()f"{x:{width}.{prec}f}" # nested, computed specf"{{literal braces}}" # doubled braces escape

Since 3.12 (PEP 701) f-strings may reuse the same quote inside, nest arbitrarily, contain backslashes and span multiple lines: f"{d["key"]}" is now legal.

Format spec mini-language

[[fill]align][sign][z][#][0][width][grouping][.precision][type]
PieceValues
align< left · > right · ^ centre · = pad after the sign
sign+ always · - negatives only · space — a space for positives
grouping, thousands · _ underscore (also every 4 digits for b o x)
type: intd b o x X c n
type: floatf F e E g G n %g is the default, % multiplies by 100
type: strs (default); .precision truncates
# / zalternate form (0x prefix) · coerce negative zero to +0 (3.11+)

The older formatters

"{} {}".format(a, b)str.format: same spec after a colon"{0} {name}".format(x, name=y)positional and keyword fields"%s scored %d%%" % (n, p)printf style; still used by loggingTemplate("$who").substitute(who=x)string.Template — safe for user-supplied templates

Logging takes %-style lazily: log.info("got %s", obj), not an f-string — the formatting is skipped entirely if the level is off.

Template strings (t-strings)

from string.templatelib import Template # 3.14, PEP 750t = t"Hello {name}" # a Template object, NOT a strt.strings # ('Hello ', '')t.values # (name,)

New in 3.14. A t"..." literal hands you the pieces before interpolation, so a library can escape them properly — the foundation for safe HTML and SQL builders. It has no __str__; you must process it.

Everyday operations

s.strip() lstrip() rstrip()trim whitespace, or any chars givens.removeprefix(p) removesuffix(x)3.9+ — safer than slicing by len()s.split() rsplit(sep, maxsplit)no arg splits on runs of whitespaces.splitlines()handles \r\n, \r, and the exotic line breaks"-".join(parts)the fast concatenation; += in a loop is O(n²)s.replace(a, b, count)every occurrence unless count givens.startswith(("a","b"))a tuple tests several at onces.find(x) / s.index(x)-1 vs ValueError when absents.casefold()aggressive lower() for caseless comparisons.encode("utf-8")→ bytes

Bytes, Text & Encodings

the str/bytes wall

Python 3 keeps text and bytes strictly apart. str is a sequence of Unicode code points with no encoding; bytes is a sequence of 0–255 integers with no meaning. The bridge is always explicit.

text = "caf\u00e9"data = text.encode("utf-8") # b'caf\xc3\xa9' — 5 bytes, 4 charstext2 = data.decode("utf-8") # back to str"a" + b"b" # TypeError. Never implicit
len("caf\u00e9") → 4code pointslen(b"caf\xc3\xa9") → 5bytesdata[0] → 99indexing bytes gives an int, not b"c"data[0:1] → b"c"slicing gives bytes — a classic surprise

Error handlers

HandlerOn bad input
strictraise UnicodeDecodeError / EncodeError — the default
replacesubstitute U+FFFD � (decode) or ? (encode)
ignoredrop the offending bytes — silent data loss
surrogateescaperound-trip undecodable bytes through lone surrogates — how Python survives filenames that are not valid UTF-8
backslashreplace\xNN escapes — good for logs
xmlcharrefreplace&#NNN;, encode only

Where the encoding comes from

open(p, encoding="utf-8")ALWAYS pass it. Otherwise the locale decideslocale.getencoding()the platform default (UTF-8 on macOS, was cp1252 on Windows)PYTHONUTF8=1 or -X utf8UTF-8 mode: force it everywheresys.getdefaultencoding()always 'utf-8' for str.encode()sys.getfilesystemencoding()for paths-X warn_default_encodingflag every open() that forgot

PEP 686 makes UTF-8 mode the default in a future release; passing encoding= explicitly is forward-compatible either way. In 3.15 the default for open() becomes UTF-8.

Unicode itself

ord("A") → 65 / chr(65) → "A"code point ↔ characterunicodedata.name(c)'LATIN SMALL LETTER E WITH ACUTE'unicodedata.normalize("NFC", s)compose; NFD decomposesunicodedata.category(c)'Lu', 'Nd', 'Zs' …s.isascii()3.7+, the cheap guard
"café" can be two different strings. NFC keeps é as one code point; NFD splits it into e + a combining accent. macOS filenames arrive in NFD. Normalise before comparing or hashing user text.

bytearray and memoryview

ba = bytearray(b"abc")mutable bytes; ba[0]=98, ba.extend(...)mv = memoryview(buf)zero-copy window into a buffermv[10:20] = datawrite in place, no slice copymv.cast("I")reinterpret as unsigned intsstruct.pack(">IH", a, b)binary layout; > big-endian, < little

Lists, Tuples & Slicing

sequences
xs = [1, 2, 3]list — ordered, mutable, heterogeneoust = (1, 2, 3)tuple — immutable; the comma makes it, not the parenssingle = (1,)one-element tuple. (1) is just 1a, b, *rest = xsunpacking; rest is always a lista, b = b, aswap, no temporary[*xs, *ys] (*t1, *t2)unpack into a new sequence

Slicing — s[start:stop:step]

xs[2:5]index 2,3,4 — stop is exclusivexs[:3] xs[3:]omit for the endsxs[-1] xs[-2]last, second-to-lastxs[::2]every second elementxs[::-1]reversed copy — works on str and tuple tooxs[:]a shallow copy (list.copy() is clearer)xs[1:3] = [9, 9, 9]slice assignment resizes the listdel xs[::2]delete a strided slice

Slices never raise IndexError — out-of-range bounds clamp. A bare index does raise. That asymmetry hides bugs; xs[5:6] silently gives [].

List methods

xs.append(x)one item at the end — amortised O(1)xs.extend(it) / xs += itevery item of an iterablexs.insert(i, x)O(n); a deque is better at the frontxs.pop() / xs.pop(0)last is O(1), first is O(n)xs.remove(x)first equal item, else ValueErrorxs.index(x, start, stop)position, else ValueErrorxs.count(x) xs.reverse() xs.clear()in placexs.sort(key=..., reverse=...)in place, returns Nonesorted(xs, key=...)a new list, works on any iterable
xs.sort() returns None. ys = xs.sort() gives you None. Same for reverse(), append(), extend() — every in-place method. Use sorted() when you want a value.

Sorting properly

sorted(words, key=str.lower)case-insensitivesorted(recs, key=lambda r: r["age"])by fieldsorted(recs, key=itemgetter("age","name"))multi-key, fastersorted(recs, key=attrgetter("age"))for objectssorted(xs, key=lambda r: (-r.score, r.name))mixed directionssorted(xs, reverse=True)whole-order reversal

Timsort is stable: equal keys keep their original order, so you can sort by secondary key first, then primary. There is no cmp=; wrap an old comparator in functools.cmp_to_key.

Copying

b = asame object — not a copy at allb = a[:] / list(a) / a.copy()shallow: new list, same elementsb = copy.deepcopy(a)recursive, handles cycles, slow
grid = [[0]*3]*3 makes three references to one row. grid[0][0]=1 changes all three. Write [[0]*3 for _ in range(3)].

Cost of the common operations

Operationlistdequedict / set
index / key lookupO(1)O(n)O(1)
append / addO(1)*O(1)O(1)
insert or pop at frontO(n)O(1)
x in sO(n)O(n)O(1)

If a membership test sits inside a loop, the container should be a set. That single change is the most common real-world Python speed-up there is.

Dicts & Sets

hash tables
d = {"a": 1, "b": 2}dict literald = dict(a=1, b=2)keyword form — identifier keys onlyd = dict(pairs)from an iterable of (k, v)dict(zip(keys, values))the standard build-from-two-listsd["a"]KeyError if absentd.get("z")None if absent; d.get("z", 0) for a defaultd.setdefault(k, []).append(x)get-or-create in one stepd |= othermerge in place (3.9+); d3 = d1 | d2 for a new dict{**d1, **d2}the older merge; later wins

Dicts preserve insertion order — an implementation detail in 3.6, guaranteed since 3.7. Equality still ignores order.

Views, iteration and deletion

d.keys() d.values() d.items()live views, not lists — they track the dictfor k, v in d.items():the idiomatic loopd.keys() & other.keys()key views support set algebra: & | - ^d.pop(k) / d.pop(k, default)remove and returnd.popitem()remove the LAST inserted pair (LIFO)reversed(d)3.8+ — newest key first
Never add or delete keys while iterating a dictRuntimeError: dictionary changed size during iteration. Iterate list(d), or build a new dict.

Keys must be hashable

Hashable = has __hash__ and the hash never changes. All immutables qualify; list, dict and set do not. True == 1 == 1.0, so they are the same key: {1: "a", True: "b"} has one entry.

The collections dicts

defaultdict(list)missing key auto-creates; the factory takes no argsCounter(words)counts; .most_common(3), +, -, &, |OrderedDict()now mostly redundant; still has move_to_end() and order-sensitive ==ChainMap(overrides, defaults)layered lookup without copyingMappingProxyType(d)read-only view (types module)
defaultdict creates on read. Merely testing dd["missing"] inserts the key. Use k in dd or dd.get(k) to look without writing.

Sets

s = {1, 2, 3}set literal. {} is an empty DICT — use set()frozenset(xs)immutable, hashable — can be a dict keya | b a & b a - b a ^ bunion, intersection, difference, symmetric differencea <= b a < bsubset, proper subset; a >= b superseta.isdisjoint(b)no shared elements, without building the intersections.add(x) s.discard(x)discard never raises; remove() doess.update(it) / s |= otherin place

Set and dict iteration order is arbitrary but deterministic within one run — and for str/bytes keys it changes between runs because hashing is salted. Set PYTHONHASHSEED=0 for reproducibility, or sort before you print.

Dict and set comprehensions

{k: v for k, v in pairs}dict comprehension{v: k for k, v in d.items()}invert a dict (values must be hashable and unique){w.lower() for w in words}set comprehension — dedupes as it goes{k: v for k, v in d.items() if v}filter out falsy values

Comprehensions, Generators & Iterators

lazy evaluation
[f(x) for x in xs]list comprehension[f(x) for x in xs if p(x)]with a filter[f(x) if p(x) else g(x) for x in xs]conditional VALUE — note the different position[(x,y) for x in a for y in b]nested loops, outer first — same order as the statements[y for row in m for y in row]flatten one level(f(x) for x in xs)GENERATOR expression — lazy, one pass, no list builtsum(x*x for x in xs)bare genexp as a sole argument needs no extra parens

Reach for a genexp when the result feeds straight into sum, any, max, join or a loop: constant memory instead of a full list. Reach for a list when you need to index it, keep it, or walk it twice.

The iterator protocol

An iterable has __iter__. An iterator has __iter__ (returning itself) and __next__, and raises StopIteration when exhausted. for calls iter() then next() until it stops.

it = iter([1, 2, 3])next(it) # 1next(it, "done") # a default instead of StopIterationlist(it) # drains what is leftiter(callable, sentinel) # call until it returns sentinel
An iterator is consumed. zip, map, filter, reversed, file objects and generators all yield once and are then empty — a second for over them does nothing. Wrap in list() if you need it twice.

Generator functions

def countdown(n): while n > 0: yield n # pauses here, resumes on the next next() n -= 1 return "liftoff" # becomes StopIteration.valuedef flatten(items): for it in items: yield from it # delegate to a sub-iterable
g.send(value)resume, making the yield expression evaluate to valueg.throw(exc)raise inside the generator at the yieldg.close()raise GeneratorExit — run the finally blocks

A generator holds its whole local state between yields. That makes it the cheapest possible coroutine, and the reason with inside a generator is dangerous: the file is only closed when the generator is exhausted, closed or collected.

Working with iterables

enumerate(xs, start=1)index and value; never write range(len(xs))zip(a, b)stops at the shortest — SILENTLYzip(a, b, strict=True)3.10+ — ValueError on unequal lengths. Use ititertools.zip_longest(a, b, fillvalue=0)pad insteadreversed(xs)needs a sequence, not any iterablesorted / min / max(xs, key=..., default=...)default= saves the empty-input crashany(...) / all(...)short-circuit; all([]) is Truesum(xs, start)numbers only — join strings, chain lists

itertools, the ones you actually use

chain(a, b) / chain.from_iterable(m)concatenate lazilyislice(it, 5)slice an iterator (no negative indices)batched(it, 3)3.12+ — fixed-size tuples, last may be shortpairwise(xs)3.10+ — (x0,x1), (x1,x2), …groupby(sorted(xs, key=k), key=k)runs of equal keys — SORT FIRSTaccumulate(xs) / accumulate(xs, max)running totalsproduct / permutations / combinationsthe combinatoricscount(1) cycle(xs) repeat(x, n)infinite — always pair with islice or takewhiletakewhile(p, xs) / dropwhile(p, xs)prefix / suffix by predicatetee(it, 2)fork an iterator (buffers — can be memory-hungry)

Control Flow

statements

Statements, with the desugaring worth carrying in your head:

a < b <= c(a < b) and (b <= c), b evaluated once, short-circuitingy = a if c else bthe conditional EXPRESSION; the only ternaryx = f() or g()returns an operand, not a bool — the Elvis idiomfor x in it: ...iter(it) then __next__ until StopIteration; the name leaks[f(x) for x in it]an implicit function with its own scope — the name does not leaka, b = b, aRHS tuple built first, then unpacked: a real simultaneous bindingwhile c: ... else: ...else runs iff the loop was not left by break

Loops

for item in iterable: if skip(item): continue if done(item): breakelse: print("loop finished without break") # the for/else clausewhile cond: ...else: # same rule: runs unless break ...

Read else as no break. It is the clean way to express searched the whole thing and found nothing without a found-flag.

for i in range(10)0..9. range(2,10,2) → 2,4,6,8; range(10,0,-1) counts downfor i, x in enumerate(xs)index and valuefor a, b in zip(xs, ys, strict=True)parallel iterationfor k, v in sorted(d.items())deterministic dict walkfor _ in range(n)_ signals a deliberately unused name
Do not mutate the sequence you are iterating. for x in xs: xs.remove(x) skips elements. Iterate over a copy (for x in xs[:]) or, better, build a new list with a comprehension.

Short-circuit operators

a and breturns a if falsy, else b — the VALUE, not a boola or breturns a if truthy, else bx = arg or []the classic default — but 0 and "" also trigger itx = arg if arg is not None else []correct when 0/"" are validnot aalways a real bool

The walrus operator

if (m := pattern.search(line)): # match once, use twice print(m.group(1))while (chunk := f.read(8192)): # read-until-empty process(chunk)[y for x in xs if (y := f(x)) is not None] # compute once, filter and keep

pass, assert, raise

passa statement that does nothing — a placeholder body...Ellipsis; used as a stub body in typed code and .pyi filesassert cond, "message"internal invariants ONLYraise ValueError("bad")the way to reject caller input
assert vanishes under python -O. Never validate arguments, permissions or user data with it. And assert (cond, "msg") with parentheses asserts a non-empty tuple — always true, always useless.

Emulating switch, before match

handlers = {"add": do_add, "del": do_del}handlers.get(cmd, do_default)() # dict dispatch, still the fastest

Pattern Matching

match · 3.10+

Structural pattern matching (PEP 634) decomposes a value and binds names in one step. It is not a C switch — the power is in destructuring, not in equality.

match command.split(): case ["quit"]: print("bye") case ["go", direction]: move(direction) # binds direction case ["drop", *items]: drop(items) # star pattern case ["look"] | ["examine"]: look() # or-pattern case [action, obj] if obj in room: do(action, obj) # guard case _: print("?") # wildcard

The pattern kinds

PatternMatches
42, "go", None, TrueLiteral. None True False compare with is, the rest with ==
nameCapture — always matches and binds. A bare name is never a constant test
_Wildcard: matches, binds nothing
[a, b, *rest]Sequence — any Sequence except str, bytes, bytearray
{"k": v, **rest}Mapping — a partial match; extra keys are fine
Point(x=0, y=y)Class pattern: isinstance check, then attributes
Point(0, y)Positional — requires __match_args__ on the class
Color.RED, mod.CONSTValue pattern — a dotted name is compared, not bound
x as p, str() as sAS pattern: match and also bind the whole
int() | float()Or-pattern; every alternative must bind the same names
A bare name always captures. case RED: does not test against your RED constant — it matches everything and rebinds RED. Use a dotted name (case Color.RED:) or a literal.

Class patterns and dataclasses

from dataclasses import dataclass@dataclassclass Point: x: int; y: intmatch p: case Point(0, 0): print("origin") case Point(x=0, y=y): print(f"on the y axis at {y}") case Point() as pt: print(pt)

@dataclass and NamedTuple set __match_args__ for you. Builtins accept one positional sub-pattern for their own value: case str() | bytes():, case int(n) if n > 0:.

Matching JSON-shaped data

match event: case {"type": "click", "pos": [x, y]}: click(x, y) case {"type": "key", "key": str(k)}: press(k) case {"type": t, **extra}: unknown(t, extra)

No case matching and no case _ is not an error — the whole statement is simply skipped. Add an explicit case _: raise ValueError(...) when silence would hide a bug.

match and case are soft keywords: existing code with variables named match still works.

Functions

def
def greet(name, greeting="Hello", *args, sep=" ", **kwargs): return f"{greeting}{sep}{name}"
FormMeaning
aPositional-or-keyword
a=1Default. Evaluated once, at definition time
*argsRemaining positionals, as a tuple
*Everything after it is keyword-only
**kwargsRemaining keywords, as a dict
/Everything before it is positional-only (3.8+)
def f(pos_only, /, standard, *, kw_only): ...f(1, 2, kw_only=3) # okf(pos_only=1, ...) # TypeError
The mutable default argument. def add(x, items=[]): shares one list across every call, forever. Write items=None and if items is None: items = [] in the body. The same applies to {}, set(), datetime.now() and any call.

Calling

f(*seq)unpack a sequence into positional argumentsf(**mapping)unpack a dict into keyword argumentsf(*a, **kw)the pass-through signature of every decorator

Return values

returnreturns None — as does falling off the endreturn a, breturns the TUPLE (a, b)x, y = f()unpack it at the call site

Annotations and lambdas

def area(r: float, unit: str = "m") -> float: ...key = lambda p: (p.last, p.first) # expression only, no statementssorted(people, key=lambda p: p.age)

PEP 8 says do not assign a lambda to a name — use def, which gives the function a real __name__ for tracebacks. Lambdas are for arguments.

Docstrings and introspection

f.__doc__ f.__name__ f.__module__metadataf.__defaults__ f.__kwdefaults__the shared default objectsf.__annotations__3.14: computed lazily (PEP 649/749)inspect.signature(f)a real Signature you can bind and inspecthelp(f)docstring + signature in the REPL

Recursion

sys.setrecursionlimit(10_000)default is 1000; there is no tail-call optimisation@functools.cacheturns exponential recursion into linear

Since 3.12 CPython no longer consumes a C stack frame per Python frame, so deep recursion raises a clean RecursionError rather than crashing the process.

Decorators & functools

higher-order

A decorator is a callable that takes a function and returns a replacement. @d above def f is exactly f = d(f).

import functoolsdef logged(fn): @functools.wraps(fn) # keep __name__, __doc__, __wrapped__ def wrapper(*args, **kwargs): print("calling", fn.__name__) return fn(*args, **kwargs) return wrapper@loggeddef work(x): return x * 2
Always @functools.wraps. Without it the wrapper hides the original's name, docstring, annotations and signature — breaking help(), tracebacks, pickling and every framework that introspects.

A decorator that takes arguments

def retry(times=3): # 3 levels: args -> fn -> wrapper def deco(fn): @functools.wraps(fn) def wrapper(*a, **kw): for attempt in range(times): try: return fn(*a, **kw) except Exception: if attempt == times - 1: raise return wrapper return deco@retry(times=5)def fetch(url): ...

Stacked decorators apply bottom-up: @a over @b over def f gives a(b(f)).

The built-in decorators

@staticmethodplain function living in the class namespace@classmethodfirst argument is the class — alternative constructors@propertycomputed attribute; then @x.setter, @x.deleter@functools.cache3.9+ unbounded memoisation; args must be hashable@functools.lru_cache(maxsize=128)bounded; .cache_info(), .cache_clear()@functools.cached_propertycompute once per instance, store in __dict__@functools.singledispatchoverload on the first argument's type via .register()@functools.total_orderingfill in the other comparisons from __eq__ and one of < > <= >=@contextlib.contextmanagerturn a generator into a with-statement@dataclasses.dataclassgenerate __init__, __repr__, __eq__@typing.overload / @overridetype-checker only; @override is 3.12+@abc.abstractmethodwith ABCMeta, block instantiation
@cache on a method keeps every self alive forever — the cache holds a strong reference, so instances never get collected. Cache module-level functions, or use cached_property.

The rest of functools

partial(f, a, kw=1)freeze arguments; partialmethod for classesreduce(op, xs, init)fold. Prefer sum/min/max/accumulate where they fitcmp_to_key(cmp)adapt a Python-2 style comparator to key=wraps / update_wrappercopy the metadata

operator, the function forms

itemgetter("name") / itemgetter(0, 2)fast key= for dicts, rows, tuplesattrgetter("a.b")dotted paths workmethodcaller("strip", "-")call a named method with fixed argsadd sub mul truediv lt eq containsevery operator as a function

Classes & the Data Model

dunders
class Account: interest = 0.02 # CLASS attribute, shared def __init__(self, owner, balance=0): self.owner = owner # INSTANCE attribute self._balance = balance # _ = internal by convention def __repr__(self): return f"Account({self.owner!r}, {self._balance!r})" @property def balance(self): return self._balance @classmethod def from_row(cls, row): return cls(row["owner"], row["bal"])
A mutable class attribute is shared by every instance. class C: items = [] then c.items.append(1) changes it for all of them. Assign per-instance in __init__.

Attribute lookup order

Data descriptors on the type → the instance __dict__ → non-data descriptors and class attributes along the MRO → __getattr__. __getattribute__ intercepts everything; __getattr__ runs only after normal lookup fails. The full algorithm, and why @property cannot be shadowed while a method can, is in Attribute Lookup, Descriptors & the MRO.

Inheritance and super()

class Savings(Account): def __init__(self, owner, balance=0, rate=0.05): super().__init__(owner, balance) # no-arg form, 3.x self.rate = rate
C.__mro__the method resolution order (C3 linearisation)isinstance(x, (A, B))type test — honours ABCs and __instancecheck__issubclass(C, A)class relationshiptype(x) is Cexact type, no subclasses

super() walks the MRO, not the parent — which is what makes cooperative multiple inheritance work. Every class in such a chain must call super() and accept **kwargs.

The dunder methods that matter

GroupMethods
Construction__new__ __init__ __del__ __init_subclass__ __set_name__
Display__repr__ (unambiguous, for you) __str__ (readable, for users) __format__
Comparison__eq__ __ne__ __lt__ __le__ __gt__ __ge__ __hash__
Container__len__ __getitem__ __setitem__ __delitem__ __contains__ __iter__ __reversed__
Numeric__add__ __radd__ __iadd__ __mul__ __neg__ __abs__ __round__ __index__
Callable / context__call__ __enter__ __exit__ __aenter__ __aexit__
Attributes__getattr__ __getattribute__ __setattr__ __delattr__ __dir__
Descriptors__get__ __set__ __delete__
Define __eq__ and Python sets __hash__ = None — your instances become unhashable and unusable as dict keys. Add __hash__ yourself, or use @dataclass(frozen=True), or eq=False.

__slots__

class Point: __slots__ = ("x", "y") # no per-instance __dict__

Smaller and faster for many small objects, and it turns typos into AttributeError instead of a silent new attribute. Cost: no arbitrary attributes, and no __weakref__ unless you list it.

Metaclasses & class creation

class C(Base, metaclass=M)M(name, bases, namespace) builds the classtype("C", (Base,), {"x": 1})make a class at runtime__init_subclass__(cls, **kw)hook every subclass — usually enough, no metaclass neededabc.ABC / @abstractmethodrefuse to instantiate until overridden

Dataclasses, Enums & Records

structured values
from dataclasses import dataclass, field@dataclass(frozen=True, slots=True, kw_only=True)class Point: x: float y: float = 0.0 tags: list[str] = field(default_factory=list) # NEVER = [] def dist(self) -> float: return (self.x**2 + self.y**2) ** 0.5

The decorator writes __init__, __repr__ and __eq__ from the annotated class attributes, in order.

OptionEffect
frozen=TrueImmutable; assignment raises. Makes it hashable (with eq=True)
slots=True3.10+ — add __slots__: smaller, faster, no stray attributes
kw_only=True3.10+ — every field keyword-only; sidesteps the default-ordering rule
order=TrueAdd __lt__ … comparing the field tuple
eq=FalseKeep identity equality and the inherited __hash__
field(default_factory=list)fresh mutable per instance — the required idiomfield(init=False, repr=False)keep it out of __init__ / __repr__field(compare=False)exclude from == and orderingfield(metadata={"units": "m"})arbitrary annotation for other tools__post_init__(self)validation and derived fieldsasdict(p) / astuple(p)deep conversion; replace(p, x=3) for a modified copyfields(Point)the Field objects — names, types, defaults
A field with a default cannot precede one without — TypeError at class creation. Reorder, give a default, or use kw_only=True.

NamedTuple — a tuple with names

from typing import NamedTupleclass Point(NamedTuple): x: float y: float = 0.0p = Point(1, 2); p.x; p[0]; x, y = p # all three workp._replace(x=9); p._asdict(); Point._fields

Immutable, indexable, unpackable and comparable as a plain tuple — ideal for return values and dict keys. collections.namedtuple("Point","x y") is the untyped original.

Enum

from enum import Enum, IntEnum, StrEnum, Flag, autoclass Color(Enum): RED = auto(); GREEN = auto(); BLUE = auto()Color.RED; Color.RED.name; Color.RED.value; Color("RED" ) # by valueColor["RED"]; list(Color); len(Color)
IntEnum / StrEnumalso a real int / str — StrEnum is 3.11+Flag / IntFlagbitwise combinable members: A | B, x in flags@uniquereject duplicate values (aliases)@member / @nonmember3.11+ — force or exclude membershipEnumMeta._missing_hook for unknown values

Since 3.11, str(Color.RED) is 'Color.RED' and f"{Color.RED}" agrees — the 3.10 mismatch is gone. IntEnum formats as its number.

Which one?

NeedUse
Mutable record with behaviour@dataclass
Immutable key / return valueNamedTuple or @dataclass(frozen=True)
Fixed set of namesEnum family
Just annotating a dict's shapeTypedDict — no runtime class at all
Validation & parsing from JSONpydantic or attrs

Exceptions

try · except* · groups
try: risky()except (KeyError, IndexError) as e: # one clause, several types log.warning("missing: %s", e)except ValueError: raise # re-raise, traceback intactelse: commit() # only if NO exceptionfinally: cleanup() # always, even on return/break

Keep the try body as small as possible, and put the code that must not be guarded in else. That is the whole point of the clause.

except Exception is broad; bare except: is worse — it swallows KeyboardInterrupt and SystemExit too. Catch the narrowest type you can actually handle. Never write except: pass.

Raising

raise ValueError("x must be positive")the plain formraise ValueError(msg) from errexplicit cause — "direct cause" in the tracebackraise ValueError(msg) from Nonesuppress the context entirelyraisebare, inside except: re-raise the current onee.add_note("while parsing row 7")3.11+ — attach context without wrapping

Raising inside an except block sets __context__ automatically, and you get During handling of the above exception, another occurred. from sets __cause__ and says so.

The hierarchy, abridged

BaseException├── SystemExit, KeyboardInterrupt, GeneratorExit└── Exception ├── ArithmeticError -> ZeroDivisionError, OverflowError ├── LookupError -> IndexError, KeyError ├── OSError -> FileNotFoundError, PermissionError, TimeoutError, │ IsADirectoryError, FileExistsError, ConnectionError ├── ValueError -> UnicodeError -> UnicodeDecodeError ├── TypeError, AttributeError, NameError, ImportError ├── RuntimeError -> RecursionError, NotImplementedError └── StopIteration, StopAsyncIteration, MemoryError, EOFError

except Exception deliberately misses KeyboardInterrupt and SystemExit, which sit beside it under BaseException. That is the design, and the reason to prefer it over a bare except.

Custom exceptions

class AppError(Exception): """Base for everything this package raises."""class ConfigError(AppError): def __init__(self, key): super().__init__(f"missing config key: {key}") self.key = key

Give a package one base class so callers can catch everything with one clause. Subclass ValueError/OSError when your error genuinely is one.

Exception groups — 3.11+

try: raise ExceptionGroup("failures", [ValueError("a"), TypeError("b")])except* ValueError as eg: # handles the ValueErrors, leaves the rest print(eg.exceptions)except* TypeError as eg: # a SECOND clause can also run ...

Several except* clauses may fire for one group — unlike except, where the first match wins. This is how asyncio.TaskGroup reports several concurrent failures at once.

Tracebacks

traceback.print_exc()current exception to stderrtraceback.format_exception(e)as a list of stringslog.exception("failed")ERROR + traceback, inside an except blocke.__traceback__ / e.__cause__ / e.__context__the chainsys.excepthooklast-resort handler for uncaught exceptions

3.11+ tracebacks carry fine-grained ^^^^ markers under the exact failing sub-expression, and 3.12+ suggests the name you probably meant.

Context Managers

with
with open("data.csv", encoding="utf-8") as f: for line in f: ...# f is closed here, even if the body raisedwith open(a) as src, open(b, "w") as dst: # several, one statement dst.write(src.read())with (open(a) as src, # 3.10+: parenthesised, open(b, "w") as dst): # so it wraps cleanly ...

with expr as name calls expr.__enter__() and binds its return value; on the way out it calls __exit__(exc_type, exc, tb). A truthy return from __exit__ swallows the exception — almost never what you want.

Writing one

from contextlib import contextmanager@contextmanagerdef chdir(path): old = os.getcwd() os.chdir(path) try: yield path # exactly one yield finally: os.chdir(old) # runs even if the body raised
The try/finally is not optional. Without it an exception in the with body propagates through the yield and your teardown never runs.

The class form

class Timer: def __enter__(self): self.t = time.perf_counter(); return self def __exit__(self, exc_type, exc, tb): self.elapsed = time.perf_counter() - self.t return False # do not suppress

contextlib

suppress(FileNotFoundError)the readable form of try/except/passclosing(obj)call .close() on anythingredirect_stdout(buf) / redirect_stderrcapture printingExitStack()a dynamic number of managers; stack.enter_context(cm)nullcontext(x)a do-nothing manager — for optional resourceschdir(p)3.11+ — the recipe above, in the stdlib (not thread-safe)asynccontextmanagerthe async twin, for async withAbstractContextManagerthe ABC to inherit from
with ExitStack() as stack: files = [stack.enter_context(open(p)) for p in paths] # all closed on exit, however many there were

Common managers you already have

open(...)filestempfile.TemporaryDirectory()and NamedTemporaryFile()threading.Lock() / Semaphore()acquire and releasesqlite3.connect(...)the CONNECTION commits/rolls back; it does not closesubprocess.Popen(...)waits for the childdecimal.localcontext(prec=50)scoped precisionunittest.mock.patch(...)and pytest.raises(ValueError)asyncio.TaskGroup()3.11+ structured concurrency

Modules, Packages & Imports

the import system
import jsonbind the module objectimport numpy as npaliasfrom pathlib import Pathbind one name from itfrom . import siblingrelative — inside a package onlyfrom .models import Userexplicit relative importfrom x import *only in the REPL; honours __all__importlib.import_module(name)import a name computed at runtime

A module is one .py file. A package is a directory Python can import — with an __init__.py (regular) or without one (namespace package, PEP 420). Modules are executed once and cached in sys.modules.

Where imports come from

sys.path = [ script's dir or '' , $PYTHONPATH , stdlib , site-packages ]
sys.paththe search list; first match winsmod.__file__where it actually came from — check this when confusedsys.modulesthe cache; del an entry to force a re-importimportlib.reload(mod)re-execute — existing references keep the OLD objectspython -v / -X importtimetrace what got imported, and how slowly
Never name a file after a stdlib module. A local random.py, json.py or email.py shadows the real one for the whole program, with baffling errors. Same for a stray __pycache__ next to a deleted source file.

__main__ and the script/module split

def main() -> int: ... return 0if __name__ == "__main__": raise SystemExit(main())

A module run directly has __name__ == "__main__"; imported, it has its own name. raise SystemExit(main()) sets the process exit status without sys.exit imports. A package with a __main__.py runs with python -m pkg.

Package layout that works

myproj/├── pyproject.toml├── src/│ └── mypkg/│ ├── __init__.py # exports; keep it thin│ ├── __main__.py # python -m mypkg│ └── core.py└── tests/ └── test_core.py

The src layout stops your tests importing the working copy instead of the installed package — the single most common packaging bug. Install with pip install -e . and imports are the same in tests, CI and production.

Circular imports

A imports B, B imports A: whichever loads second sees a half-built module and fails on a name. Fixes, in order of preference: move the shared thing to a third module; import inside the function that needs it; use import a (module object) instead of from a import thing; or guard type-only imports:

from typing import TYPE_CHECKINGif TYPE_CHECKING: from .models import User # never imported at runtime

Module conventions

__all__ = ["a", "b"]what "from m import *" exports; also a documentation contract_privateone underscore: internal by convention__version__or importlib.metadata.version("pkg")importlib.resources.files("pkg")read data files shipped in a package — works inside a zip/wheel

Type Hints & Annotations

typing · PEP 695

Annotations are not checked at runtime. They are read by mypy, pyright/Pylance and your editor, and by libraries that opt in (dataclasses, pydantic, FastAPI).

def parse(text: str, limit: int = 10) -> list[dict[str, int]]: ...names: list[str] = []config: dict[str, str | int] = {}maybe: str | None = None # 3.10+; was Optional[str]

Since 3.9 the built-in containers are generic — write list[int], dict[str, int], tuple[int, ...], set[str]. typing.List and friends are deprecated. Since 3.10, X | Y replaces Union[X, Y] and X | None replaces Optional[X].

The vocabulary

Anyopt out of checking (contagious — use sparingly)object"anything", but you must narrow before using it — the safe AnyNever / NoReturnthis function never returns normallyLiteral["r", "w"]exactly these valuesFinal / ClassVarnever rebound / a class attribute, not a fieldCallable[[int, str], bool]a function type; ... for any argumentsIterable / Sequence / Mappingaccept broadly — import from collections.abcIterator[int] / Generator[Y, S, R]generator return typesTypedDicta dict with known keys; total=False, Required, NotRequiredProtocolstructural typing — "has these methods", no inheritanceSelf3.11+ — the correct return type for fluent methodsAnnotated[int, "metres"]carry metadata alongside the typeTypeGuard / TypeIstell the checker what a predicate narrows to (TypeIs is 3.13+)Unpack / TypeVarTuple / ParamSpecvariadic generics; signature-preserving decorators

Generics, the modern syntax

def first[T](xs: list[T]) -> T: # 3.12+, PEP 695 return xs[0]class Box[T]: def __init__(self, item: T) -> None: self.item = itemtype Row = dict[str, int | None] # 3.12+ type alias statement

The old spelling — T = TypeVar("T"), class Box(Generic[T]) — still works and is what you need below 3.12.

Annotations are lazy now

3.14 implements PEP 649/749: annotations are evaluated on demand, not at definition time. Forward references just work, and the from __future__ import annotations workaround is no longer needed.

class Node: parent: Node | None # legal in 3.14 with no quotes, no __future__from annotationlib import get_annotations, Formatget_annotations(Node, format=Format.VALUE) # evaluate themget_annotations(Node, format=Format.STRING) # as source text

Structural typing with Protocol

from typing import Protocol, runtime_checkable@runtime_checkableclass Closeable(Protocol): def close(self) -> None: ...def shut(x: Closeable) -> None: x.close() # any object with close()

Checking and stubs

mypy src/ --strictthe strictest useful setting for new codepyright / basedpyrightfaster, what VS Code's Pylance runsreveal_type(x)checker-only: prints the inferred typecast(int, x)assert a type to the checker; no runtime effectassert isinstance(x, str)narrowing that is also checked at runtime*.pyi / py.typedstub files; the marker that says "this package is typed"# type: ignore[arg-type]silence one error, named

Files, Paths & Serialisation

pathlib · json · csv
from pathlib import Pathp = Path("~/data/log.csv").expanduser()p.parent / "other.csv" # / joins — the whole point of pathlibp.name p.stem p.suffix p.parts p.parent p.anchorp.exists() p.is_file() p.is_dir() p.stat().st_sizep.resolve() p.absolute() p.relative_to(base)p.with_suffix(".bak") p.with_name("new.csv") p.with_stem("new")
p.read_text(encoding="utf-8")whole file — also read_bytes()p.write_text(s, encoding="utf-8")create or truncate; write_bytes()p.mkdir(parents=True, exist_ok=True)the idempotent mkdir -pp.iterdir()one levelp.glob("*.py") / p.rglob("*.py")rglob recursesp.walk()3.12+ — os.walk over Pathsp.unlink(missing_ok=True)delete a filep.rename(q) / p.replace(q)replace overwrites, rename may notshutil.rmtree(d) / copytree(a, b)recursive delete / copyPath.cwd() Path.home()class methods

open()

with open(path, "r", encoding="utf-8", newline="") as f: ...
ModeMeaning
r w aread · truncate-or-create · append
xexclusive create — FileExistsError if it exists
b / tbinary (gives bytes, no encoding) / text, the default
+add the other direction: r+, w+, a+
for line in f:lazy, keeps the newline — use .rstrip("\n")f.read() f.readline() f.readlines()whole / one / listf.write(s)no newline added; writelines() adds none eitherf.seek(0) f.tell() f.flush()position; flush does not fsyncos.fsync(f.fileno())actually durable on disk

Pass newline="" when reading or writing CSV, otherwise embedded newlines in quoted fields are mangled.

JSON

json.load(fp) / json.loads(s)parsejson.dump(obj, fp) / json.dumps(obj)serialisedumps(o, indent=2, sort_keys=True)readable and diffabledumps(o, ensure_ascii=False)keep real Unicode instead of \uXXXXdumps(o, default=str)last-resort coercion for dates and Decimalsloads(s, object_hook=fn)build your own objects from each dict

JSON has no date, tuple, set or bytes type; tuples become lists and all keys become strings. Round-tripping a dict with int keys does not give you int keys back.

CSV

import csvwith open(p, newline="", encoding="utf-8") as f: for row in csv.DictReader(f): # row is a dict keyed by header print(row["name"])with open(q, "w", newline="", encoding="utf-8") as f: w = csv.DictWriter(f, fieldnames=["name", "qty"]) w.writeheader(); w.writerows(rows)

The other formats

tomllib.load(f)3.11+ read-only TOML — open the file in BINARY modeconfigparserINI filessqlite3a real database in one file, in the stdlibpickleany Python object — NEVER unpickle untrusted datashelvea dict backed by pickle on diskzipfile / tarfile / gziparchives and compressionPyYAML / ruamel.yamlYAML is third-party; use safe_load()

Regular Expressions

re
import repat = re.compile(r"(\w+)@(\w+)\.com") # compile once, reusem = pat.search(text)if m: m.group(0), m.group(1), m.groups(), m.span()
Always use a raw string. "\b" is a backspace character; r"\b" is the word-boundary you meant. This is the number-one regex bug in Python.

The API

re.search(p, s)first match ANYWHERE — usually what you wantre.match(p, s)anchored at the START onlyre.fullmatch(p, s)the whole stringre.findall(p, s)list of strings, or of tuples if there are groupsre.finditer(p, s)lazy iterator of Match objects — prefer itre.sub(p, repl, s, count=0)repl may be a function taking the Matchre.subn(p, r, s)(result, number of replacements)re.split(p, s, maxsplit=0)captured groups appear in the outputre.escape(s)quote a literal for use inside a pattern

Syntax

AtomMatchesAtomMatches
.any char except newline\d \Ddigit / non-digit
^ $start / end of string (or line with re.M)\w \Wword char [a-zA-Z0-9_] / not
[abc] [^abc]class / negated class\s \Swhitespace / not
\b \Bword boundary / not\A \Zabsolute start / end
* + ?0+, 1+, 0 or 1{m,n}m to n times
*? +? ??lazy — as few as possible*+ ++possessive, no backtracking (3.11+)
(...)capturing group(?:...)group, no capture
(?P<name>...)named group → m["name"](?P=name)backreference by name
a|balternation, leftmost wins\1backreference to group 1
(?=...) (?!...)lookahead: positive / negative(?<=...) (?<!...)lookbehind (fixed width)

Flags

re.I / IGNORECASEcase-insensitivere.M / MULTILINE^ and $ match at every linere.S / DOTALL. also matches newlinere.X / VERBOSEignore whitespace and # comments IN the patternre.A / ASCII\w \d \s become ASCII-only (they are Unicode by default)(?i) (?m) (?x)inline, at the very start of the pattern
pat = re.compile(r""" (?P<user>\w+) # local part @ # at (?P<host>[\w.]+) # domain""", re.VERBOSE)

Match objects

m[0] / m.group()the whole match; m[1] the first groupm.groupdict()named groups as a dictm.start() m.end() m.span(1)offsetsm.expand(r"\2-\1")template substitution
Catastrophic backtracking. Nested quantifiers over overlapping alternatives — (a+)+b, (\s*\w+)*$ — can take exponential time on a non-matching input. Anchor the pattern, make quantifiers lazy or possessive, or use a parser. Do not parse HTML, JSON or CSV with regex.

async / await

asyncio

Cooperative concurrency in one thread: a coroutine runs until it awaits, hands control back to the event loop, and resumes when its result is ready. It makes I/O concurrent — network calls, disk, subprocesses. It does nothing for CPU-bound code.

import asyncioasync def fetch(session, url): async with session.get(url) as r: return await r.text()async def main(): async with asyncio.TaskGroup() as tg: # 3.11+ tasks = [tg.create_task(fetch(s, u)) for u in urls] return [t.result() for t in tasks]asyncio.run(main()) # ONE call, at the top level

The rules

RuleWhy
await only inside async defOtherwise SyntaxError
Calling a coroutine does nothingIt returns a coroutine object; it runs when awaited or scheduled
One asyncio.run() per programIt creates and closes the loop. Never call it from inside a coroutine
Never block in a coroutinetime.sleep, requests.get, heavy CPU: they stall every task
Hold a reference to bare taskscreate_task keeps only a weak reference — unreferenced tasks can vanish. TaskGroup solves this

Running things together

async with asyncio.TaskGroup() as tg:3.11+ structured: waits for all, cancels the rest on failure, raises an ExceptionGroupawait asyncio.gather(*aws)results in order; return_exceptions=True to collect failuresawait asyncio.wait_for(aw, timeout=5)TimeoutError and cancellationasync with asyncio.timeout(5):3.11+ — a deadline around a whole blockawait asyncio.sleep(0)yield to the loop onceasyncio.as_completed(aws)results as they finishawait asyncio.to_thread(fn, *a)3.9+ — run blocking code off the looploop.run_in_executor(pool, fn)the same with your own pool

Async iteration and context managers

async for row in cursor: # __aiter__ / __anext__ ...async with conn.transaction(): # __aenter__ / __aexit__ ...async def gen(): yield 1 # an async generatorresults = [x async for x in gen()]

Synchronisation and queues

asyncio.Lock() Semaphore(n) Event()async versions — NOT the threading onesasyncio.Queue(maxsize)the producer/consumer backbonetask.cancel()raises CancelledError inside the taskexcept asyncio.CancelledError: cleanup; raisealways re-raise itasyncio.shield(aw)protect from outer cancellationasyncio.current_task() / all_tasks()introspection
CancelledError inherits from BaseException, not Exception, so except Exception will not eat it — but except BaseException will. If you catch it, re-raise.

Choosing

WorkloadTool
Many network callsasyncio + httpx/aiohttp, or anyio/trio
A few blocking calls in async codeasyncio.to_thread
Blocking I/O, no async libraryThreadPoolExecutor
CPU-boundProcessPoolExecutor, or the free-threaded build

Threads, Processes & the GIL

concurrency

In the default build one global interpreter lock lets only one thread execute Python bytecode at a time. Threads still win for I/O — the GIL is released around every blocking call, and by C extensions such as NumPy. For CPU work in the standard build you need processes.

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutorwith ThreadPoolExecutor(max_workers=8) as ex: # I/O bound for result in ex.map(fetch, urls): ...with ProcessPoolExecutor() as ex: # CPU bound futures = [ex.submit(crunch, chunk) for chunk in chunks] for f in as_completed(futures): print(f.result()) # re-raises the worker's exception here

concurrent.futures is the right default: the same API for both, exceptions surface at .result(), and the with block joins everything.

threading

t = Thread(target=fn, args=(), daemon=True)daemon threads die with the processt.start() / t.join(timeout)never call run() directlyLock() / RLock()mutual exclusion; RLock is re-entrantwith lock:the only safe way to hold oneEvent() Condition() Semaphore(n) Barrier(n)coordinationqueue.Queue()thread-safe hand-off — prefer it to shared statethreading.local()per-thread storage
x += 1 is not atomic. Read, add, store — the GIL can switch between them. Any shared mutable state needs a lock, or a queue.Queue.

multiprocessing

Process(target=fn, args=())a real OS process, its own memoryPool(4).map(fn, items)the older pool APIQueue() Pipe() Value() Array()IPC primitivesSharedMemory / shared_memory.ShareableList3.8+ zero-copy sharingManager().dict()proxied shared objects (slow, convenient)set_start_method("spawn")the default on macOS and Windows
Everything crossing a process boundary must be picklable — no lambdas, no closures, no open files. And with the spawn start method the child re-imports your module, so process-creating code must sit behind if __name__ == "__main__": or you fork-bomb yourself.

Free-threaded CPython

PEP 703 builds (python3.14t) run with no GIL at all: real parallel threads for pure Python. Officially supported since 3.14, but a separate build — single-threaded code is somewhat slower and C extensions must opt in.

sys._is_gil_enabled()True on a normal buildpython3.14t -X gil=0 prog.pyrun without the GILsysconfig.get_config_var("Py_GIL_DISABLED")which build is this

Subinterpreters

3.14 ships concurrent.interpreters (PEP 734): interpreters in one process, each with its own GIL, so they run in parallel without the pickling cost of separate processes.

from concurrent import interpretersinterp = interpreters.create()interp.exec("print('hello from a subinterpreter')")

Which one

SymptomReach for
Waiting on the network or diskthreads, or asyncio
Burning CPU in pure Pythonprocesses, or the free-threaded build
Burning CPU in NumPy / Cthreads — those release the GIL
Thousands of concurrent socketsasyncio — threads do not scale that far

Testing, Debugging & Logging

the loop you live in

pytest — the de facto standard

def test_area(): assert area(2) == pytest.approx(12.566, rel=1e-3)@pytest.mark.parametrize("n,expected", [(0, 1), (1, 1), (5, 120)])def test_factorial(n, expected): assert factorial(n) == expecteddef test_rejects_negative(): with pytest.raises(ValueError, match="positive"): factorial(-1)@pytest.fixturedef db(tmp_path): # built-in fixture: a temp dir conn = sqlite3.connect(tmp_path / "t.db") yield conn # teardown after the yield conn.close()
pytest -qquiet; -v verbose, -x stop at the first failurepytest -k "parse and not slow"select by name expressionpytest --lf / --fflast-failed / failed-firstpytest -sdo not capture stdoutpytest --pdbdrop into the debugger at the failurepytest --cov=mypkgcoverage (pytest-cov)pytest -p no:randomly -n autoplugins: ordering, xdist parallelism

Built-in fixtures worth knowing: tmp_path, capsys, monkeypatch, caplog, request. Shared fixtures live in conftest.py.

unittest — in the stdlib

class TestArea(unittest.TestCase): def setUp(self): self.r = 2 def test_area(self): self.assertAlmostEqual(area(self.r), 12.566, 3) def test_raises(self): with self.assertRaises(ValueError): area(-1)python -m unittest discover -s tests -v
unittest.mock.patch("mod.func")as a decorator or context managerMagicMock(return_value=..., side_effect=...)side_effect may raise or be a listm.assert_called_once_with(1, k=2)and .call_args, .call_countpatch.object(obj, "attr")patch WHERE IT IS USED, not where defineddoctest.testmod()run the examples in your docstrings

Debugging

breakpoint()3.7+ — drops into pdb (or PYTHONBREAKPOINT's debugger)n s c r qnext, step, continue, return, quitl ll w u dlist, long list, where, up, down the stackp expr / pp exprprint / pretty-printb file:12, condconditional breakpoint; tbreak for one-shotinteracta full REPL in the current framepython -m pdb -c continue prog.pypost-mortem on a crash
f"{value=}"the fastest print-debugging there isreprlib.repr(big)truncated repr of a huge structurepprint.pp(obj, width=100)readable nested datatraceback.print_stack()how did I get herefaulthandler.enable()traceback on segfault or SIGABRTtracemalloc.start()where the memory went

Logging, not print

import logginglog = logging.getLogger(__name__) # per module, alwayslogging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)-8s %(name)s: %(message)s")log.debug("x=%s", x) # lazy: not formatted unless DEBUG is onlog.exception("failed") # ERROR plus the traceback; in except only

Levels: DEBUG 10 · INFO 20 · WARNING 30 (the default) · ERROR 40 · CRITICAL 50. Libraries should only ever getLogger(__name__) and never call basicConfig — configuring logging is the application's job.

Measuring before optimising

python -m timeit -s "setup" "stmt"micro-benchmarks, best of manytime.perf_counter()monotonic high-resolution wall clockpython -m cProfile -s cumtime prog.pywhere the time wentpython -X importtime -c "import mypkg"slow startupruff check . / ruff format .lint and format, very fast (PyPI)mypy src/static types

Idioms, Traps & Performance

read this twice

The traps that catch everyone

TrapFix
def f(x, items=[])One list for all calls, forever. Use items=None
[[0]*3]*3Three references to one row. Use a comprehension
lambda: i in a loopLate binding — all closures see the last i. Use lambda i=i:
Mutating a list while iteratingSkips items. Iterate a copy or build a new list
x is 256 works, x is 257 does notis is identity. Use ==
0.1 + 0.2 != 0.3math.isclose, or Decimal for money
except: passHides KeyboardInterrupt and your bugs. Name the exception
assert user.is_adminVanishes under -O. Raise instead
zip(a, b) with different lengthsSilently truncates. Pass strict=True
s += x in a loopO(n²) copying. "".join(parts)
x in big_list inside a loopO(n) each time. Make it a set
A local file named random.pyShadows the stdlib for the whole program
d[k] += 1 on a new keyKeyError. Counter or defaultdict(int)
open() with no encoding=Locale-dependent; breaks on another machine
Comparing NFC and NFD textNormalise first (unicodedata.normalize)
copy() of a nested structureShallow. copy.deepcopy when it matters
datetime.now() for durationsWall clock jumps. time.perf_counter()
Naive datetimesAlways attach a tzinfo; store UTC, display local

Idioms that read well

for i, x in enumerate(xs):never range(len(xs))for a, b in zip(xs, ys, strict=True):parallel iterationif not items:not len(items) == 0x is Nonenot x == None", ".join(str(x) for x in xs)build strings oncewith open(p) as f:never a bare open()Counter(words).most_common(10)not a hand-rolled tallyd.get(k, default) / d.setdefault(k, [])not try/except KeyErrorsorted(xs, key=itemgetter(1))not a comparatorfirst = next(it, None)not list(it)[0]path.read_text(encoding="utf-8")not open/read/closea, b = b, anot a temporary

Making it faster, in order

  1. Measure. cProfile for where, timeit for how much. Intuition about Python performance is usually wrong.
  2. Fix the algorithm. O(n²) → O(n) beats every micro-optimisation combined.
  3. Pick the right container. set/dict for membership, deque for both ends, bisect for sorted lookup, array/bytes for homogeneous numbers.
  4. Do less in the loop. Hoist attribute lookups and method references out; local names are faster than globals.
  5. Cache. @functools.cache on a pure function is often a 100× win for free.
  6. Move the loop into C. str.join, sum, map, itertools, bytes.translate, NumPy.
  7. Then reach for processes, the free-threaded build, Cython, PyPy or a Rust extension.
EAFP is not a style preference any more. Zero-cost exceptions (3.11) mean a try that does not raise costs nothing at all, while if k in d: d[k] pays two lookups. Prefer try/except KeyError when the miss is rare, d.get when it is not. See Execution Model & Bytecode.
3.11 through 3.14 got materially faster on their own. The specialising adaptive interpreter (PEP 659), zero-cost exceptions, cheaper frames and, in 3.14, a tail-calling interpreter: upgrading the interpreter is usually the cheapest optimisation available.

Object Layout & Memory

CPython

Every value is a heap-allocated PyObject: a refcount, a type pointer, and whatever the type adds. There are no unboxed scalars, no tagged pointers and no value types — [1, 2, 3] is an array of three pointers to three PyLongObjects. That single fact explains most of Python’s memory profile and most of its speed profile.

sys.getsizeof(0)28 bytes — header plus one 30-bit digitsys.getsizeof([])56; a list is a header plus a pointer array it over-allocatessys.getsizeof({})64 for an empty compact dictsys.getsizeof(object())16: refcount + type pointer, and nothing else

Reclamation is two mechanisms, not one

Reference counting frees acyclic garbage deterministically, at the moment the last reference drops — which is why with works and why CPython needs no finalisation queue. A generational mark-and-sweep collector exists only to break cycles among container objects. Three generations; a collection is triggered by allocation-minus-deallocation thresholds, not by memory pressure.

gc.get_threshold()(2000, 10, 10) since 3.12 — gen0 count, then ratiosgc.freeze()move the current heap out of gen0: cuts copy-on-write churn after forkgc.disable()safe only if you truly create no cycles; refcounting still runsweakref.ref(obj)a reference the counter ignores — the way out of a cycle

3.12 moved the collector to a mostly-incremental scheme and made gen0 threshold-scaled; 3.13 removed the untracking of tuples that never contain containers. Neither changes the contract: do not rely on __del__ ordering, and use weakref.finalize when you need a callback.

Interning and small-object caches

-5 … 256preallocated int singletons — the reason `x is 256` "works"identifier-like str literalsinterned at compile time; sys.intern() forces it at runtimeempty tuple, 1-char latin-1 strsingletonsfree listsper-type recycling for small tuples, dicts, frames, floats
Interning is an optimisation, never a semantic. Identity tests on ints and strings pass in the REPL and fail on real input. The one thing interning is good for: sys.intern() on millions of repeated dict keys turns every subsequent comparison into a pointer compare.

The container implementations

TypeRepresentation
listGrowable pointer array, over-allocated ~12.5% — amortised O(1) append, O(n) front insert
dictCompact (PEP 509 era): a sparse index array of small ints plus a dense insertion-ordered entry array. Hence ordering, and ~30% less memory than 3.5
setOpen addressing, no dense array — so no ordering guarantee, unlike dict
strPEP 393 flexible representation: 1, 2 or 4 bytes per code point, chosen by the widest character. O(1) indexing, and one astral character quadruples the string
instancesA __dict__ pointer, usually key-sharing with the class; __slots__ replaces it with a fixed array

Hashing of str and bytes is SipHash-1-3 with a per-process random key (PEP 456) — set PYTHONHASHSEED for reproducibility. Small-int hashing is the identity, modulo 2**61-1, which is why hash(1) == hash(1.0) == hash(Decimal(1)) and why they collide as dict keys on purpose.

Execution Model & Bytecode

the interpreter

Source is parsed by a PEG parser (PEP 617, since 3.9) into an AST, compiled to a stack-based bytecode over a control-flow graph, and executed by an eval loop. There is no user-visible IR between AST and bytecode, and no ahead-of-time type information.

import disdis.dis(lambda a, b: a + b * 2)# LOAD_FAST a / LOAD_FAST b / LOAD_CONST 2# BINARY_OP 5 (*) / BINARY_OP 0 (+) / RETURN_VALUE

The specialising adaptive interpreter (PEP 659, 3.11+)

Hot instructions rewrite themselves in place into type-specialised variants guarded by an inline cache: BINARY_OP becomes BINARY_OP_ADD_INT, LOAD_ATTR becomes a slot offset, CALL becomes a direct dispatch. A guard failure deoptimises back to the generic form. This is quickening, not JIT compilation — it happens per code object, after a warm-up counter, with no machine code emitted.

dis.dis(f, adaptive=True)show the specialised opcodes actually runningf.__code__.co_codethe raw bytes; co_consts, co_names, co_varnames alongsidedis.get_instructions(f)the Instruction stream, with jump targets resolvedsys._getframe().f_lastithe instruction pointer, if you must

The corollary for optimisation: monomorphic code is fast, polymorphic code is not. A loop that sees one type per site specialises and stays specialised; one that alternates types thrashes the guards and pays for both.

Frames, calls and exceptions

3.11–3.14 changeConsequence
Frames are a contiguous data stack, not heap objectsCalls got roughly 3× cheaper; sys._getframe() materialises one lazily
Python-to-Python calls are inlined in the eval loopNo C recursion per Python frame, so deep recursion raises cleanly
Zero-cost exceptionstry costs nothing when nothing raises; the handler table is out of line. EAFP is now genuinely free on the happy path
3.13 experimental copy-and-patch JIT; 3.14 tail-calling interpreterReal machine code, off by default; the tail-call build is ~5–10% on top
sys.monitoring (PEP 669, 3.12)Event-based instrumentation with no cost when unused — what profilers should target instead of settrace

What the compiler will and will not do

constant folding2**10, "a"*3, frozenset literals in `in` testspeephole + CFG passesdead code, jump threading, redundant loadsdocstring / assert removalonly under -O / -OONO inlining of Python functionsevery call is a real callNO escape analysis, NO unboxingthe int in your loop is still a heap objectNO tail-call optimisationrecursion depth is bounded, deliberately

Names are the other cost: LOAD_FAST indexes an array, LOAD_GLOBAL probes two dicts (now with an inline cache), and an attribute is a full lookup. Hoisting append = out.append out of a loop is not superstition — it turns two lookups per iteration into an array index.

Attribute Lookup, Descriptors & the MRO

the protocol

obj.x is type(obj).__getattribute__(obj, "x"). The default implementation, in order:

  1. Walk type(obj).__mro__ for "x". If found and it is a data descriptor (defines __set__ or __delete__), call its __get__ and stop.
  2. Look in obj.__dict__. If present, return it.
  3. Fall back to the class attribute found in step 1. If it is a non-data descriptor (only __get__ — every plain function), call __get__; otherwise return it as-is.
  4. Raise AttributeError, which triggers type(obj).__getattr__ if the class defines one.
That precedence is the whole design. @property is a data descriptor, so it beats the instance dict — which is why assigning to a read-only property raises instead of shadowing it. A plain method is a non-data descriptor, so obj.method = something does shadow it.

The descriptor protocol

class Positive: # a reusable validating attribute def __set_name__(self, owner, name): self.name = "_" + name def __get__(self, obj, objtype=None): return self if obj is None else getattr(obj, self.name) def __set__(self, obj, value): if value <= 0: raise ValueError(self.name) setattr(obj, self.name, value)

Functions, property, classmethod, staticmethod, __slots__ entries and functools.cached_property are all descriptors. Bound methods are created on every access: f.__get__(obj, cls) returns a fresh MethodType, which is why obj.m is obj.m is False.

C3 linearisation

The MRO is the C3 merge of the class, its bases’ MROs and the list of bases — the unique order preserving local precedence and monotonicity. If no such order exists, the class statement itself raises TypeError.

class A: passclass B(A): passclass C(A): passclass D(B, C): passD.__mro__ # D, B, C, A, object — the diamond, resolved breadth-ish

super() is a proxy over the MRO of type(self), starting after the class where the call textually appears — not over the base class. Cooperative multiple inheritance therefore requires every class in the chain to call super() and to tolerate keyword arguments it does not consume. The zero-argument form works because the compiler inserts an implicit __class__ cell.

Class creation

metaclass=MM(name, bases, ns, **kwds) — usually more power than you need__prepare__returns the namespace mapping the class body executes in__init_subclass__a classmethod hook on the PARENT; covers most metaclass use cases__set_name__called on every descriptor in the namespace, after the class exists__mro_entries__PEP 560 — how Generic[T] can appear in a bases listABCMeta.registervirtual subclassing: isinstance passes, no inheritance, no mixins

A class body is executed once, in its own scope, and is not in the lexical chain of methods defined inside it — which is why a comprehension in a class body cannot see the class-level names it sits beside.

Operator Dispatch & Coercion

the binary protocol

There is no numeric tower coercion and no implicit conversion. a + b is resolved entirely by type slots:

  1. If type(b) is a proper subclass of type(a) and overrides __radd__, try b.__radd__(a) first — the subclass gets right of way.
  2. Otherwise try type(a).__add__(a, b).
  3. If that returns the NotImplemented singleton, try type(b).__radd__(b, a).
  4. If that also returns NotImplemented, raise TypeError.
Return NotImplemented, do not raise, and never return NotImplementedError. Returning the sentinel is what lets the other operand’s reflected method run; raising short-circuits a protocol that was about to succeed. Special methods are looked up on the type, never on the instance — assigning obj.__add__ does nothing.

Augmented assignment

a += btries __iadd__ first; falls back to a = a + blist.__iadd__exists — extends in place and rebinds to the same objecttuple has noneso t += x builds a new tuplet[0] += [1]mutates AND raises TypeError: the store fails after the mutation succeeds

Rich comparison

The six comparisons have no default relationship. __lt__ and __gt__ are each other’s reflections; the reflected call happens whenever the first returns NotImplemented. Only __eq__ gets a default (identity), and only __ne__ is derived. @functools.total_ordering fills in the rest at a small speed cost.

a < b < cdesugars to (a < b) and (b < c) with b evaluated oncesorted() needs only __lt__list.sort compares with < and nothing else__hash__ = Noneset automatically when you define __eq__NaNbreaks reflexivity: x != x, so containers fall back to identity first

Containers short-circuit on identity before calling __eq__x in [nan_obj] is True for that exact object and False for an equal-valued one. That is the documented behaviour, not a bug.

Numeric protocol details

__index__lossless int conversion — what indexing, slicing, hex() and range() require__int__ / __trunc__lossy; int() accepts these, a[i] does not__bool__ then __len__in that order; a __len__ raising is a real source of bugsbool is a subclass of intTrue + True == 2, and isinstance(True, int) is True__round__ / __floor__ / __ceil__math.floor delegates; float uses banker's rounding at .5

The container protocols worth knowing

iter(x)__iter__, else the legacy __getitem__(0..) sequence protocolk in x__contains__, else __iter__, else __getitem__ — three fallbacks deeplen(x)__len__ only; no fallback, and it must fit in Py_ssize_tx[1:2]__getitem__ receives a slice object, not two argumentscollections.abc mixinsinherit Sequence and supply __getitem__+__len__: the rest is free

What Changed: 3.9 → 3.14

version notes

Which release introduced the thing you are about to use — the question that actually matters when a target machine is not on the newest Python.

VerHeadline additions
3.9
Oct 2020
dict | dict merge · str.removeprefix/removesuffix · list[int] as an annotation · zoneinfo · functools.cache · math.lcm
3.10
Oct 2021
match statement (PEP 634) · X | Y union types · parenthesised context managers · zip(strict=True) · itertools.pairwise · much better error messages
3.11
Oct 2022
10–60% faster (PEP 659) · exception groups and except* · add_note() · asyncio.TaskGroup and timeout · tomllib · Self · StrEnum · fine-grained error locations in tracebacks
3.12
Oct 2023
PEP 695 genericsdef f[T](), type X = ... · f-strings unshackled (PEP 701) · @override · itertools.batched · pathlib.Path.walk · per-interpreter GIL · distutils removed
3.13
Oct 2024
New interactive REPL (colour, multiline, paste) · experimental free-threaded build (PEP 703) · experimental JIT · TypeIs · warnings.deprecated · PEP 667 locals() semantics · more dead batteries removed
3.14
Oct 2025
Deferred annotations (PEP 649/749) and the annotationlib module · t-strings (PEP 750) · free-threading officially supported · concurrent.interpreters (PEP 734) · except without parentheses (PEP 758) · tail-calling interpreter · safe external debugger attach (PEP 768)

Gone, or going

distutilsremoved in 3.12 — use setuptools / hatchling and pyproject.tomlthe "dead batteries"PEP 594: aifc, audioop, cgi, cgitb, chunk, crypt, imghdr, mailcap, msilib, nis, nntplib, ossaudiodev, pipes, sndhdr, spwd, sunau, telnetlib, uu, xdrlib — all gone in 3.13impremoved 3.12; use importlibasynchat, asyncore, smtpdremoved 3.12; use asynciotyping.List, Dict, Tuple…deprecated aliases — use the builtinsdatetime.utcnow() / utcfromtimestamp()deprecated 3.12; use datetime.now(UTC)locale.getdefaultlocale()deprecated; use getlocale()/getencoding()unittest.makeSuite, assertEqualsthe camelCase aliases are gone in 3.12from __future__ import annotationsno longer needed in 3.14

Writing for more than one version

import sysif sys.version_info >= (3, 11): import tomllibelse: import tomli as tomllib # the backport, same API

requires-python = ">=3.11" in pyproject.toml stops pip installing your package where it cannot run. Backports worth knowing: tomli, typing_extensions (every new typing feature, on old Pythons), exceptiongroup, backports.zoneinfo.

Release cadence: one minor version each October, two years of bugfixes, then three of security-only — five years in all. 3.9 reached end of life in October 2025.

How This Reference Was Built

provenance

Every name in the index was checked against CPython 3.14.6 running on this machine — the build script imports each module and asserts that each documented attribute exists, so a name that was renamed or removed cannot survive a rebuild. Signatures in the hover tooltips come from inspect.signature where the object exposes one.

python3 -VVpython3 -c "import sys; print(len(sys.stdlib_module_names))"python3 verify.py # asserts every indexed name resolves

The prose half is written from the Language Reference, the Library Reference and the PEPs they cite. Four well-known sheets were used as coverage checks — what a Python sheet has to contain, not what it should say: Real Python’s cheat sheet for the beginner's spine, gto76’s Comprehensive Python Cheatsheet for the density target and the standard-library breadth, Eric Matthes’ Python Crash Course sheets for what a learner looks up first, and Martelli, Ravenscroft, Holden & McGuire, Python in a Nutshell (O’Reilly, 4th ed.) for the object model and the shape of the reference half.

Where those sources disagree with 3.14, 3.14 wins — several widely-copied sheets still show typing.List, datetime.utcnow(), distutils and pre-PEP-604 unions.

The four interpreter cards are pitched at a reader who wants the mechanism, and are drawn from the CPython source and its design documents rather than from the tutorial layer: the data model chapter of the Language Reference, Objects/dictobject.c and Objects/listobject.c for the container representations, PEP 393 (flexible string representation), PEP 456 (SipHash), PEP 659 (the specialising adaptive interpreter), PEP 669 (sys.monitoring), PEP 703 (free-threading) and PEP 734 (subinterpreters), plus Michele Simionato’s write-up of C3 linearisation. Sizes, thresholds and the specialised opcodes were read off this interpreter with sys.getsizeof, gc.get_threshold and dis.dis(…, adaptive=True), not quoted.

The Traps card and the version table are the parts that are not in the official documentation at all: they are collected from what actually goes wrong.

Filter box at the top matches names, signatures and descriptions across the whole index — type async, path, 3.12, deprecated or hash to slice it by topic. Press / to jump to it, Esc to clear. The page is one self-contained file: no fonts, no scripts, no analytics, and it prints to landscape.

Library & Builtin Index

Every name worth remembering, grouped by what it does — type in the filter box to narrow it

Built-in Functions

71

Types & conversion

intx=0 -> int; int(s, base)
floatto float; accepts "inf", "nan", "1_0.5"
complexcomplex(re, im) or complex("1+2j")
booltruthiness as True/False; bool is a subclass of int
strtext; str(b, encoding) decodes bytes
bytesimmutable bytes; bytes(s, "utf-8"); bytes(5) -> 5 zeros
bytearraymutable bytes
memoryviewzero-copy view of a buffer
listmutable sequence from any iterable
tupleimmutable sequence
dictmapping; dict(pairs), dict(**kw), dict(other)
setmutable set; set() is the only empty-set literal
frozensetimmutable, hashable set
rangelazy arithmetic sequence, O(1) memory
objectthe base of every class; object() is a unique sentinel
typetype(x) the class; type(n, bases, ns) builds one
slicethe object built by a[i:j:k]; .start .stop .step

Iteration

iterget an iterator; iter(callable, sentinel) is the 2-arg form
nextadvance an iterator; next(it, default) avoids StopIteration
enumerateyields (index, value); start= sets the first index
zipparallel iteration; strict=True rejects unequal lengths
maplazy f(x) over one or more iterables
filterlazy items where pred is true; filter(None, xs) drops falsy
reversedreverse iterator; needs a sequence or __reversed__
sortednew sorted list from any iterable; stable
sumadd up numbers; start= for the initial value
minsmallest; key= and default= supported
maxlargest; key= and default= supported
anyTrue if any item is truthy; any([]) is False
allTrue if every item is truthy; all([]) is True
lennumber of items; calls __len__
aiterasync iterator from an async iterable (3.10+)
anextawait the next item of an async iterator (3.10+)

Numbers & text

absabsolute value; modulus for complex
divmod(quotient, remainder) in one call
powa**b; pow(a, b, mod) is fast modular exponentiation
roundround to n digits; ties go to EVEN
binint -> "0b1010"
octint -> "0o17"
hexint -> "0xff"
ordcharacter -> code point
chrcode point -> character
asciirepr with non-ASCII escaped
reprunambiguous string for developers; calls __repr__
formatformat(x, spec) — the engine behind f-strings
hashhash value; equal objects must hash equal
ididentity (the address in CPython)

Attributes & objects

getattrgetattr(o, "x", default) — attribute by name
setattrsetattr(o, "x", v) == o.x = v
hasattrTrue if getattr does not raise
delattrdelattr(o, "x") == del o.x
dirattribute names; no argument = current scope
vars__dict__ of an object; no argument = locals()
isinstancetype test, honours subclasses and ABCs
issubclassclass relationship test
callableTrue if the object can be called
superproxy to the next class in the MRO
propertymanaged attribute; usually used as @property
classmethodmethod receiving the class as the first argument
staticmethodplain function inside a class body

I/O & execution

printsep=" " end="\n" file=sys.stdout flush=False
inputread a line from stdin; the prompt is optional
openopen a file; ALWAYS pass encoding= for text
evalevaluate one expression — never on untrusted input
execexecute statements — never on untrusted input
compilesource -> code object; also used for AST work
globalsthe module namespace dict, writable
localslocal namespace; an independent snapshot since 3.13
breakpointdrop into the debugger; PYTHONBREAKPOINT selects it
helpinteractive documentation
exitREPL only — use raise SystemExit in scripts

Keywords, Operators & Precedence

44

Hard keywords (35)

False True Nonethe three singletons; None is the absence of a value
and or notboolean operators; and/or return an operand, not a bool
if elif elseconditional statement; also the a if c else b expression
for while break continueloops; break skips the else clause
def return yieldfunction; yield makes it a generator
lambdasingle-expression anonymous function
classclass definition
try except finally raiseexception handling; finally always runs
with ascontext manager; as binds __enter__ result
import fromthe import statement
global nonlocalrebind a module-level / enclosing-function name
passdo nothing — a syntactic placeholder
delunbind a name, key, attribute or slice
assertdebug check — removed by python -O
inmembership test; also the for-loop separator
isidentity test — use only for None and sentinels
async awaitcoroutine definition and suspension

Soft keywords

match casepattern matching (3.10+); still usable as names
_the wildcard pattern inside case; elsewhere a normal name
typethe type alias statement, type X = int (3.12+)

Precedence, loosest first

:=walrus — assignment expression (lowest)
lambdalambda expression
if – elseconditional expression
orshort-circuit or
andshort-circuit and
not xboolean not
in, <, ==, iscomparisons, membership, identity — all chain
|bitwise or; also union types and dict merge
^ &bitwise xor, and
<< >>shifts
+ -addition, subtraction
* @ / // %multiplication, matrix-multiply, division, floor, modulo
+x -x ~xunary plus, minus, bitwise not
**exponent — binds tighter than unary minus on its left
await xawait expression
x[i] x(...) x.attrsubscript, call, attribute (tightest)

Augmented & special

+= -= *= /=in-place where the type allows it, else rebind
//= %= **= @=floor, modulo, power, matmul in place
&= |= ^= >>= <<=bitwise in place
*a, **kwunpacking in calls, literals and assignment targets
@decorator syntax; also __matmul__ (NumPy)
->return annotation
...the Ellipsis singleton — the idiomatic stub body
f"" r"" b"" t""string prefixes: format, raw, bytes, template (3.14)

str — every method

47

Trim, split, join

stripstrip(chars=None) — both ends; chars is a SET of characters
lstripleft end only
rstripright end only — the usual fix for trailing newlines
removeprefixremoveprefix(p) — 3.9+; returns self if absent
removesuffixremovesuffix(s) — 3.9+; safer than s[:-len(x)]
splitsplit(sep=None, maxsplit=-1) — no sep splits on whitespace runs
rsplitsplit from the right; useful with maxsplit=1
splitlinessplitlines(keepends=False) — handles \r\n and \r
partitionpartition(sep) -> (head, sep, tail); empty sep parts if absent
rpartitionpartition at the LAST occurrence
join"-".join(iterable_of_str) — the fast concatenation

Search & test

findfind(sub, start, end) -> index or -1
rfindlast occurrence, or -1
indexlike find but raises ValueError
rindexlike rfind but raises ValueError
countcount(sub, start, end) — non-overlapping occurrences
startswithstartswith(prefix_or_tuple, start, end)
endswithendswith(suffix_or_tuple, start, end)
isalphaletters only, and not empty
isdigitdigits; isdecimal is stricter, isnumeric looser
isdecimaldecimal digits only — the one int() accepts
isnumericincludes fractions and Roman numerals
isalnumletters or digits
isspacewhitespace only, and not empty
islowerhas cased characters and all are lower case
isupperhas cased characters and all are upper case
istitletitle-cased
isascii3.7+ — every code point below 128; "" is True
isprintableno non-printable characters
isidentifieris a valid Python name (check keyword.iskeyword too)

Case

lowerlower case
upperupper case
casefoldaggressive lower() for caseless comparison (ß -> ss)
capitalizefirst character upper, rest lower
titleEvery Word Capitalised — mangles apostrophes
swapcaseinvert the case of every character

Transform & pad

replacereplace(old, new, count=-1) — every occurrence by default
translatetranslate(table) — bulk character mapping, very fast
maketransstr.maketrans(a, b, delete) builds that table
expandtabsexpandtabs(tabsize=8)
centercenter(width, fillchar=" ")
ljustljust(width, fillchar) — left aligned
rjustrjust(width, fillchar) — right aligned
zfillzero-pad on the left, keeping a leading sign
encodeencode(encoding="utf-8", errors="strict") -> bytes
format"{}".format(...) — the pre-f-string formatter
format_mapformat from a mapping without copying it (works with defaultdict)

Sequences: list, tuple, range

29

list methods

appendappend(x) — one item at the end, amortised O(1)
extendextend(iterable) — same as +=
insertinsert(i, x) — O(n); use a deque for the front
removeremove(x) — first equal item, else ValueError
poppop(i=-1) — remove and return; pop(0) is O(n)
clearempty it in place
indexindex(x, start, stop) -> position, else ValueError
countcount(x) — number of equal items
sortsort(*, key=None, reverse=False) — in place, returns None
reversein place, returns None
copyshallow copy — same as xs[:]

Slicing

s[i]item; negative counts from the end; IndexError if out of range
s[i:j]from i up to but not including j; bounds clamp, never raise
s[i:j:k]stride k; s[::2] every second, s[::-1] reversed
s[a:b] = itslice assignment — may change the list length
del s[a:b]delete a slice
s.__getitem__(slice(a,b,c))what the syntax actually calls

Shared sequence operations

x in smembership; O(n) for a list, O(1) for a set or dict
s + tconcatenation — a new object
s * nrepetition — REFERENCES, not copies, of the elements
len(s) min(s) max(s)size and extremes
s.index(x) s.count(x)also on tuple, str, bytes and range

tuple & range

tuple.countcount(x)
tuple.indexindex(x, start, stop)
rangerange(stop) / range(start, stop, step) — lazy, O(1) memory
range.startalso .stop and .step
range membership5 in range(0, 10**9) is O(1), not a scan
namedtuplecollections.namedtuple("P", "x y") — fields by name
NamedTupletyping.NamedTuple — the annotated, subclassable form

dict

27

Methods

getget(k, default=None) — never raises
setdefaultsetdefault(k, default) — get, inserting the default if absent
poppop(k[, default]) — remove and return; KeyError without a default
popitemremove and return the LAST inserted pair (LIFO since 3.7)
updateupdate(other, **kw) — merge in place; later wins
keysa live view; supports set algebra: & | - ^
valuesa live view; not a set (values need not be hashable)
itemsa live view of (key, value) pairs — the loop you want
clearempty it in place
copyshallow copy
fromkeysdict.fromkeys(keys, value) — ONE shared value object

Syntax & operators

d[k]lookup; KeyError if absent — calls __missing__ if defined
d[k] = vinsert or replace
del d[k]remove; KeyError if absent
k in dmembership — tests KEYS, and is O(1)
d1 | d2merged copy (3.9+); d |= other merges in place
{**d1, **d2}the older merge idiom
{k: v for ...}dict comprehension
reversed(d)3.8+ — newest key first
dict(zip(ks, vs))build from two parallel sequences

collections mappings

defaultdictdefaultdict(list) — missing keys are CREATED on read
Countermultiset of counts; .most_common(n), .total(), + - & |
OrderedDictorder-sensitive ==, .move_to_end(k, last=True)
ChainMapChainMap(overrides, defaults) — layered lookup, no copy
UserDictsubclass this, not dict, when overriding behaviour
MappingProxyTypetypes.MappingProxyType(d) — a read-only view
TypedDicttyping — annotate a dict shape; total=False, NotRequired

set & frozenset

21

Methods

addadd(x) — one element
removeremove(x) — KeyError if absent
discarddiscard(x) — never raises
popremove and return an arbitrary element
clearempty it
copyshallow copy
updateupdate(*others) — in place union; also |=
intersection_updatein place &= ; also difference_update, symmetric_difference_update

Algebra

a | bunion — a.union(b) accepts any iterable, | needs a set
a & bintersection
a - bdifference
a ^ bsymmetric difference — in one or the other, not both
a <= bsubset; a < b proper subset; a.issubset(b)
a >= bsuperset; a.issuperset(b)
a.isdisjoint(b)no common elements, without building the intersection
{x for x in it}set comprehension — dedupes as it builds

Notes

set()the empty set — {} is an empty DICT
frozensetimmutable and hashable: can be a dict key or a set element
elements must hashno lists or dicts inside a set
order is arbitrarydeterministic within a run; str hashing is salted per run
d.keys() & otherdict key views do set algebra too

bytes, bytearray & binary

22

bytes / bytearray

decodedecode("utf-8", errors="strict") -> str
hexb"\xff".hex() -> "ff"; hex(sep="-") groups it
fromhexbytes.fromhex("ff 00") — whitespace ignored
split rsplitsame as str, but every argument is bytes
strip startswiththe whole str API exists, bytes-flavoured
translatetranslate(table, delete=b"") — bulk byte mapping
b[0]an int, not bytes; b[0:1] gives bytes
bytearray.extendalso append, insert, pop, remove, +=

struct — C layouts

struct.packpack(fmt, *values) -> bytes
struct.unpackunpack(fmt, buf) -> tuple; the size must match exactly
struct.unpack_fromread at an offset, no slicing
struct.calcsizebytes a format needs
byte order< little, > big, ! network, = native, @ native+padding
format codesb B h H i I l L q Q f d s p ? x

memoryview & buffers

memoryviewzero-copy window; slicing does not copy
mv.castcast("I") — reinterpret the element type
mv.tobytesmaterialise a copy; .tolist() for ints
mv.releaseor use it as a context manager
array.arrayarray("d", xs) — compact homogeneous numbers
io.BytesIOan in-memory binary file; io.StringIO for text
base64b64encode / b64decode / urlsafe_b64encode
binasciihexlify, unhexlify, crc32

Special (dunder) Methods

58

Object lifecycle

__new__cls, *args — allocate; override for immutables and singletons
__init__self, *args — initialise; must return None
__del__finaliser; timing is not guaranteed. Use a context manager
__init_subclass__cls hook run for every subclass — usually beats a metaclass
__set_name__owner, name — a descriptor learns the attribute name it got
__class_getitem__makes MyClass[int] work (generic aliases)

Display & conversion

__repr__unambiguous, ideally eval-able; the fallback for __str__
__str__readable — what print() and str() use
__format__self, spec — what format() and f-strings call
__bytes__bytes(obj)
__bool__truthiness; falls back to __len__, then True
__int__ __float__int(obj), float(obj)
__index__lossless int for indexing, slicing, hex(), range()
__complex__complex(obj)

Comparison & hashing

__eq__x == y; return NotImplemented to defer to the other operand
__ne__derived from __eq__ automatically — rarely written
__lt__ __le__< and <= ; @total_ordering fills in the rest
__gt__ __ge__> and >= ; Python uses the reflected operand too
__hash__set to None automatically when you define __eq__

Containers & iteration

__len__len(x); must return a non-negative int
__getitem__x[k]; slices arrive as slice objects
__setitem__x[k] = v
__delitem__del x[k]
__contains__k in x; falls back to iteration if absent
__iter__iter(x); return an iterator (often self, or a generator)
__next__the iterator itself; raise StopIteration when done
__reversed__reversed(x)
__missing__dict subclasses only — what defaultdict overrides
__length_hint__an estimate, for pre-sizing

Numeric operators

__add__ __sub__+ and - ; __mul__ __truediv__ __floordiv__ __mod__
__radd__ __rsub__reflected: called when the LEFT operand refuses
__iadd__ __isub__in-place += -= ; must return the result
__pow__ __divmod__** and divmod()
__matmul__the @ operator (NumPy)
__neg__ __pos__ __abs__unary -, +, abs()
__invert__~x
__and__ __or__ __xor__bitwise; also __lshift__ __rshift__
__round__ __trunc__round(), math.trunc(); also __floor__ __ceil__

Attributes, calls, context

__getattr__only when normal lookup FAILS — the cheap dynamic hook
__getattribute__every attribute access — easy to make infinite
__setattr__every assignment; use object.__setattr__ inside it
__delattr__del obj.x
__dir__what dir(obj) reports
__call__makes the instance callable — obj(...)
__enter__ __exit__the with statement; a truthy __exit__ swallows the exception
__aenter__ __aexit__async with
__aiter__ __anext__async for
__await__makes an object awaitable

Descriptors & class data

__get__instance, owner — how property, classmethod and methods work
__set__defining it makes a DATA descriptor, which beats the instance dict
__delete__del instance.attr
__slots__fixed attribute names; no per-instance __dict__
__dict__the instance namespace; absent under __slots__
__class__ __bases__ __mro__type, parents, resolution order
__match_args__the tuple naming positional sub-patterns for case Cls(a, b)
__copy__ __deepcopy__hooks for the copy module
__reduce__ __getstate__pickling protocol; __setstate__ to restore
__annotations__lazily computed in 3.14 — read it via annotationlib

Formatting & f-strings

34

f-string syntax

f"{expr}"any expression, evaluated in the enclosing scope
f"{x=}"prints "x=value" — the debugging form (3.8+)
f"{x!r}"repr(); !s str(); !a ascii()
f"{x:spec}"format spec after the colon
f"{x:{w}.{p}f}"nested, computed width and precision
f"{{}}"doubled braces are literal braces
nested quotes3.12+ (PEP 701): f"{d["k"]}" and backslashes are now legal
t"{x}"3.14 template string — gives you .strings and .values, not text

Format spec fields

fill+align< left, > right, ^ centre, = pad after the sign; any fill char
sign+ always, - negatives only, " " space for positives
z3.11+ — coerce negative zero to positive zero
#alternate form: 0b/0o/0x prefixes, keep the decimal point
0zero-pad — shorthand for fill 0 with align =
widthminimum field width
,thousands separator
_underscore separator; every 4 digits for b, o, x
.precisiondigits after the point; max characters for strings

Presentation types

ddecimal integer
b o x Xbinary, octal, hex lower / upper
cthe character at that code point
nlocale-aware number
f Ffixed point; default precision 6
e Escientific
g Ggeneral — the default for floats: fixed or scientific by magnitude
%multiply by 100 and append a percent sign
sstring; the default for everything else
datetimestrftime codes go straight in the spec: f"{now:%Y-%m-%d}"

The other formatters

str.format"{0} {name}".format(a, name=b)
str.format_mapformat from a mapping — works with defaultdict
% operator"%s scored %5.2f%%" % (n, p) — still what logging takes
string.Template$name substitution; safe for user-supplied templates
textwrap.fillwrap to a width; .dedent() strips common indentation
pprint.ppreadable nested structures; width=, sort_dicts=
reprlib.reprtruncated repr of something huge

Exceptions

52

BaseException (not Exception)

BaseExceptionthe root; catch this only to re-raise
SystemExitraised by sys.exit(); carries the exit code
KeyboardInterruptCtrl-C — deliberately NOT under Exception
GeneratorExitsent into a generator by close()
BaseExceptionGroup3.11+ — a group that may hold BaseExceptions

Common

Exceptionthe base for everything you should normally catch
ValueErrorright type, wrong value — int("x")
TypeErrorwrong type, or a bad call signature
KeyErrormissing mapping key
IndexErrorsequence index out of range
AttributeErrorno such attribute; carries .name and .obj (3.10+)
NameErrorunbound name; suggests a near miss since 3.12
UnboundLocalErrora local read before assignment — the classic scope bug
ZeroDivisionErrorx / 0 or x % 0
StopIterationiterator exhausted; .value carries a generator return
StopAsyncIterationthe async twin
RuntimeErrornothing more specific fits
NotImplementedErroran abstract method that was not overridden
RecursionErrorthe recursion limit was hit
MemoryErrorallocation failed
AssertionErroran assert failed
LookupErrorthe base of KeyError and IndexError
ArithmeticErrorbase of ZeroDivisionError, OverflowError, FloatingPointError

OS & I/O

OSErrorthe base for system errors; .errno, .strerror, .filename
FileNotFoundErrorENOENT
FileExistsErrorEEXIST — what mode "x" raises
PermissionErrorEACCES / EPERM
IsADirectoryErrorand NotADirectoryError
TimeoutErrora system-level timeout; asyncio reuses this since 3.11
ConnectionErrorbase of ConnectionReset/Refused/Aborted, BrokenPipeError
InterruptedErrora syscall was interrupted by a signal
BlockingIOErrora non-blocking operation would have blocked
EOFErrorinput() hit end of file
IOErroran alias of OSError, kept for compatibility

Import, syntax, unicode

ImportError.name and .path say what failed
ModuleNotFoundErrorthe module does not exist at all
SyntaxErrorcompile time; .lineno .offset .text
IndentationErrorand TabError, its subclass
UnicodeDecodeErrorbytes -> str failed; .encoding .object .start .reason
UnicodeEncodeErrorstr -> bytes failed
ReferenceErrora weak reference proxy outlived its object
SystemErroran internal CPython error — report it
PythonFinalizationError3.13+ — an operation refused during interpreter shutdown

Groups & warnings

ExceptionGroup3.11+ — several exceptions at once; .exceptions, .subgroup()
except*match inside a group; several clauses may run
add_note3.11+ — e.add_note(str) appends to the traceback
Warningthe base warning class
DeprecationWarninghidden by default outside __main__ — run with -W default
UserWarningwhat warnings.warn() raises by default
RuntimeWarning ResourceWarningdubious behaviour; an unclosed file or socket
SyntaxWarninge.g. "is" with a literal, an invalid escape sequence
EncodingWarning-X warn_default_encoding: an open() with no encoding=

os & the Process Environment

35

Environment & process

os.environa mutable mapping; writes call putenv, so children inherit them
os.environ.getget(k, default) — the only safe read
os.getenvthe same, as a function
os.getpidand os.getppid()
os.cpu_countlogical CPUs; os.process_cpu_count() (3.13+) honours affinity
os.sched_getaffinitythe CPUs this process may actually run on (Linux)
os.forkPOSIX only; unsafe alongside threads — the reason spawn is the default
os.execvreplace the process image
os.wait / waitpidreap children; os.WIFEXITED and friends decode the status
os._exitimmediate exit, no cleanup — the only correct exit after fork()
os.umaskthe file-creation mask
os.nicescheduling priority

Files & descriptors

os.statst_mode st_size st_mtime st_ino st_dev st_nlink; follow_symlinks=
os.lstatdo not follow the final symlink
os.scandirDirEntry objects that cache stat — much faster than listdir+stat
os.listdirnames only
os.walktop-down by default; mutate dirnames in place to prune the walk
os.makedirsmakedirs(p, exist_ok=True)
os.remove / unlinkdelete a file; os.rmdir for an empty directory
os.renameatomic within one filesystem; os.replace overwrites portably
os.link / symlinkhard and soft links; os.readlink to resolve one
os.chmod / chownmode and ownership
os.openraw fd with O_ flags; O_CREAT|O_EXCL is the atomic-create idiom
os.dup2redirect a file descriptor
os.pipereturns (read_fd, write_fd)
os.fsyncforce to disk — f.flush() alone is not durable
os.fspaththe PathLike protocol: accept str, bytes or Path
os.urandomCSPRNG bytes from the OS

os.path (prefer pathlib)

os.path.joinan absolute component discards everything before it
os.path.abspathnormalises without resolving symlinks; realpath resolves them
os.path.basenameand dirname, split, splitext
os.path.existsisfile, isdir, islink, getsize, getmtime
os.path.expanduserexpand ~ ; expandvars expands $VAR
os.path.commonpaththe shared prefix, path-component-wise
os.sep / os.linesepplatform separators; os.devnull

sys, Runtime & Interpreter Internals

39

sys

sys.argvargv[0] is the script name
sys.paththe import search list; sys.path_hooks and path_importer_cache below it
sys.modulesthe import cache — delete an entry to force re-execution
sys.stdin / stdout / stderrTextIOWrapper; .buffer gives the raw binary stream
sys.exitraises SystemExit; a str argument is printed and exits 1
sys.version_infoa named tuple — compare as a tuple, never parse sys.version
sys.platform"darwin", "linux", "win32"
sys.implementationname, version, cache_tag — CPython vs PyPy
sys.maxsize2**63-1: the largest container index, not the largest int
sys.float_infoIEEE-754 limits: .epsilon .max .dig .mant_dig
sys.getsizeofshallow size in bytes; add __sizeof__ for your own types
sys.getrefcountCPython reference count — one higher than you expect
sys.setrecursionlimitframe limit, default 1000
sys.settrace / setprofilethe hooks pdb, coverage and profilers build on
sys.excepthooklast-resort handler; sys.unraisablehook for __del__ failures
sys.internforce string interning — worth it for millions of repeated keys
sys.stdlib_module_names3.10+ — every stdlib module name, frozen at build time
sys._is_gil_enabled3.13+ — False on a free-threaded build
sys.monitoring3.12+ (PEP 669) — near-zero-cost instrumentation API

Object model & memory

gc.collectrun the cyclic collector; refcounting already freed the acyclic
gc.get_objectsevery tracked object — the blunt instrument for leak hunting
gc.get_referrerswho is keeping this alive
gc.freezemove current objects out of collection — cuts copy-on-write after fork
gc.disablesafe only if you are certain you create no cycles
tracemalloc.startper-allocation tracebacks; .take_snapshot().compare_to()
weakref.ref / proxya reference the collector ignores
weakref.WeakValueDictionarycache that does not pin its values; WeakKeyDictionary too
weakref.finalizea callback at collection — more predictable than __del__
resource.getrusagePOSIX: maxrss, user and system time

Bytecode & the compiler

dis.disdisassemble a function, method, class or code string
dis.get_instructionsthe Instruction stream, programmatically
dis.Bytecodean object interface over the same; .info() for the code metadata
code objectsf.__code__: co_varnames co_consts co_names co_flags co_stacksize
compile()source -> code; flags=ast.PyCF_ONLY_AST gives an AST instead
ast.parsethe real parser; ast.dump, ast.unparse (3.9+), NodeVisitor
ast.literal_evalsafely evaluate a literal — the correct eval() for data
symtablethe compiler symbol table: scopes, bindings, closures
py_compile / compileallproduce .pyc without running
sysconfig.get_config_varbuild configuration, incl. Py_GIL_DISABLED

pathlib & the Filesystem

41

Path components

Path.namefinal component; .stem without the suffix; .suffix / .suffixes
Path.parent.parents is a sequence of ancestors
Path.partsa tuple of components; .anchor is the drive+root
Path.with_suffixwith_name, with_stem (3.9+) — return new Paths
Path.joinpathor just the / operator
Path.relative_toValueError if it is not a subpath; walk_up= in 3.12+
Path.is_relative_to3.9+ — the boolean form
Path.as_posixforward slashes on any platform; .as_uri() for file://
PurePathpath algebra with no filesystem access — safe for other platforms
Path.match / full_matchglob-style test; full_match is 3.13+

Query & traverse

Path.existsis_file, is_dir, is_symlink, is_socket, is_fifo
Path.statst_size, st_mtime, st_mode; follow_symlinks=False for lstat
Path.resolveabsolute, symlinks resolved; strict=False by default
Path.iterdirone level, arbitrary order
Path.globglob("**/*.py") recurses; case sensitivity follows the platform
Path.rglobglob with an implied leading **/
Path.walk3.12+ — os.walk semantics over Paths
Path.cwd / Path.homeclass methods

Mutate

Path.read_textread_text(encoding="utf-8"); read_bytes()
Path.write_textcreate or truncate; write_bytes()
Path.openthe same signature as the builtin
Path.mkdirmkdir(parents=True, exist_ok=True)
Path.touchcreate or update the mtime
Path.unlinkunlink(missing_ok=True); rmdir for empty directories
Path.renamemay fail across filesystems; .replace() overwrites atomically
Path.chmodand .symlink_to(), .hardlink_to()

shutil, glob, tempfile

shutil.copy2copy with metadata; copy() without; copyfile() contents only
shutil.copytreedirs_exist_ok=True (3.8+), ignore=ignore_patterns(...)
shutil.rmtreerecursive delete; onexc= handler in 3.12+
shutil.moverename, falling back to copy+delete across devices
shutil.whichresolve a command on PATH — do not shell out to `which`
shutil.disk_usage(total, used, free)
shutil.make_archiveand unpack_archive — zip, tar, gztar, bztar, xztar
shutil.get_terminal_sizecolumns and lines, honouring COLUMNS
glob.globglob(p, recursive=True) for **; iglob is lazy
fnmatch.fnmatchthe shell-glob matcher itself; .translate() gives a regex
tempfile.TemporaryDirectorya context manager that removes the tree
tempfile.NamedTemporaryFiledelete_on_close= in 3.12+; has a .name
tempfile.mkstempa low-level fd, created atomically with O_EXCL
fileinput.inputiterate several files or stdin, sed-style; inplace=True
os.replace + fsyncthe atomic-write recipe: write a temp file, fsync, replace

subprocess, signal & argparse

32

subprocess

subprocess.runthe one to use: run(argv, capture_output=True, text=True, check=True)
CompletedProcess.returncode .stdout .stderr .args
check=Trueraises CalledProcessError — .stdout survives on the exception
text=Truedecode with locale encoding; pass encoding= to be explicit
timeout=raises TimeoutExpired; the child is NOT killed for you
shell=Truea shell parses the string — an injection risk. Pass a list instead
subprocess.Popenwhen you need streaming; use .communicate(), not .wait()+read
Popen.communicatethe only deadlock-free way to drain both pipes
PIPE / DEVNULL / STDOUTstderr=subprocess.STDOUT merges the streams
subprocess.check_outputthe older shorthand; run(..., capture_output=True) is clearer
cwd= / env=env= REPLACES the environment; pass {**os.environ, ...}
asyncio.create_subprocess_execthe non-blocking equivalent

signal

signal.signalinstall a handler; it runs between bytecodes, in the main thread
SIGINT SIGTERM SIGHUPCtrl-C, polite termination, hangup
SIGKILL SIGSTOPcannot be caught or ignored
signal.alarmSIGALRM after n seconds — a crude timeout
signal.SIG_IGNignore; SIG_DFL restores the default
signal.set_wakeup_fdhow asyncio makes signals safe on its loop
atexit.registerrun at normal interpreter exit — not on SIGKILL or os._exit

argparse & friends

ArgumentParserArgumentParser(prog, description, epilog, formatter_class)
add_argumentname or -f/--flag; dest, type, default, help, metavar
action=store, store_true, store_const, append, count, version, extend
nargs=N, "?" optional, "*" any, "+" one or more
type=any callable; type=Path and type=argparse.FileType("r") both work
choices= / required=validate the value; force an option
add_subparsersgit-style subcommands; set_defaults(func=...) then args.func(args)
add_mutually_exclusive_groupat most one of these
parse_known_argsleave unrecognised arguments for someone else
parser.errorexit 2 with usage — the conventional CLI failure
argparse.BooleanOptionalAction3.9+ — gives you --flag and --no-flag
click / typerdecorator-driven CLIs; typer builds them from type hints
rich / textualterminal formatting and full TUIs

itertools

21

Infinite

countcount(start=0, step=1) — arithmetic, and works with floats
cyclerepeat an iterable forever; it buffers the whole thing
repeatrepeat(x, times=None) — the fast way to feed a constant to map()

Terminating

chainconcatenate; chain.from_iterable(m) flattens one level lazily
isliceislice(it, stop) / (it, start, stop, step) — no negative indices
batched3.12+ — fixed-size tuples; strict=True in 3.13 rejects a short tail
pairwise3.10+ — overlapping adjacent pairs, the sliding window of 2
accumulaterunning fold; accumulate(xs, operator.mul, initial=1)
groupbyconsecutive runs by key — SORT by the same key first
compresscompress(data, selectors) — a boolean mask
filterfalsethe complement of filter
takewhilethe leading run that satisfies the predicate
dropwhileeverything after it stops satisfying the predicate
starmapstarmap(f, iterable_of_arg_tuples) — f(*args)
teetee(it, n) — n independent iterators; buffers the divergence
zip_longestpad the short ones with fillvalue

Combinatorics

productthe Cartesian product; repeat=n for n-fold. Nested loops, flattened
permutationspermutations(it, r) — ordered, n!/(n-r)! of them
combinationsunordered, no repeats — C(n, r)
combinations_with_replacementmultisets of size r
more-itertoolsPyPI — windowed, chunked, unique_everseen, flatten, first_true

functools & operator

26

functools

cache3.9+ unbounded memoisation; equivalent to lru_cache(maxsize=None)
lru_cachelru_cache(maxsize=128, typed=False); .cache_info(), .cache_clear()
cached_propertycomputed once, stored in the instance __dict__; needs no __slots__ conflict
partialfreeze leading positional and any keyword arguments
partialmethodthe same, inside a class body
reducereduce(f, it, initial) — the left fold
wrapscopy __name__/__doc__/__wrapped__/__annotations__ onto a wrapper
update_wrapperthe function form of the same
singledispatchgeneric function dispatching on the first argument type; .register()
singledispatchmethodthe same for methods; stack it under @classmethod
total_orderingderive the remaining rich comparisons from __eq__ plus one
cmp_to_keyadapt an old three-way comparator to a key= function

operator

itemgetteritemgetter(1) or itemgetter("a","b") — a C-speed key=
attrgetterattrgetter("a.b.c") follows dotted paths
methodcallermethodcaller("strip", "-")
add sub mul truedivand floordiv, mod, pow, neg, abs
eq ne lt le gt gethe comparisons as functions
and_ or_ xor invertbitwise; lshift, rshift
containscontains(seq, x) — note the reversed argument order vs `in`
getitem setitem delitemsubscription as functions
iadd iand ...the in-place variants
call3.11+ — operator.call(f, *a)

Idioms

max(d, key=d.get)the key with the largest value
sorted(rows, key=itemgetter(2, 0))multi-column sort, stable, C speed
reduce(operator.or_, sets)union of many sets
functools.reduce vs sumsum() is C-level; reduce with add is not — prefer sum

collections, heapq, bisect, array

26

collections

dequeO(1) at both ends; maxlen= makes a ring buffer. .rotate(), .appendleft()
CounterCounter(it), .most_common(n), .total() (3.10+), + - & | as multisets
defaultdictthe factory takes no arguments; reading a missing key INSERTS it
OrderedDictorder-sensitive equality and .move_to_end() — otherwise dict suffices
ChainMapa stack of mappings; .new_child(), .maps
namedtuplenamedtuple("P", "x y", defaults=(0,)); ._replace, ._asdict, ._fields
UserDict UserList UserStringsubclass these to override behaviour reliably
collections.abcIterable Iterator Sequence Mapping Set Hashable Callable Awaitable

heapq — binary heap over a list

heappushO(log n); the list IS the heap, index 0 is the minimum
heappoppop the smallest, O(log n)
heapifyO(n) in place — cheaper than n pushes
heapreplacepop then push, one sift; heappushpop is push then pop
nlargest / nsmallestnsmallest(k, it, key=) — O(n log k), beats sorting for small k
mergemerge sorted iterables lazily — an external merge sort in one call
max-heap trickpush negated keys, or (-priority, tiebreak, item) tuples

bisect — sorted sequences

bisect_leftleftmost insertion point, O(log n)
bisect_rightrightmost; bisect is an alias
insort_left / insort_rightinsert keeping order — the search is O(log n), the insert O(n)
key= argument3.10+ — bisect on a derived key without decorating
grade lookup idiombisect(breakpoints, score) indexes into a table of labels

array & friends

array.arrayarray("d", xs) — typecodes b B h H i I l L q Q f d
arr.frombytes / tobytesand .frombuffer, .tofile, .fromfile
queue.Queuethread-safe FIFO; LifoQueue, PriorityQueue, SimpleQueue
graphlib.TopologicalSorter3.9+ — topological sort with a static and a parallel API
enum.Flaga bitset with names
numpy.ndarraywhen the data is numeric and large — vectorise, do not loop

math, statistics, random

34

math

floor ceil trunctrunc rounds toward zero; // floors
sqrt isqrt cbrtisqrt is exact integer sqrt; cbrt is 3.11+
exp log log2 log10log(x, base) for an arbitrary base; log1p/expm1 near zero
sin cos tan asin acos atan2radians; atan2(y, x) resolves the quadrant
degrees radiansconversion
hypot disthypot(*coords) is n-dimensional; dist(p, q) for two points
gcd lcmvariadic since 3.9
factorial comb permcomb(n, k) and perm(n, k) are 3.8+
iscloseisclose(a, b, rel_tol=1e-09, abs_tol=0.0) — the correct float ==
isnan isinf isfinitenan != nan, so you need isnan
fsumexact summation; sum() of floats accumulates error
prod3.8+ — the multiplicative sum()
nextafter ulp3.9+ — step through the float lattice
copysign fmod remainderfmod follows C (sign of the dividend), % does not
inf nan pi e tauconstants
cmaththe same for complex; cmath.phase, polar, rect

statistics

mean fmeanfmean is float-only and much faster (3.8+)
medianmedian_low, median_high, median_grouped
mode multimodemultimode returns every joint winner
stdev variancesample; pstdev and pvariance for a whole population
quantilesquantiles(data, n=4) — quartiles; method="inclusive"
correlation covariance3.10+; linear_regression() too
NormalDistNormalDist.from_samples(xs); .cdf, .inv_cdf, .overlap
geometric_mean harmonic_meanthe other two Pythagorean means

random

random.randomuniform [0.0, 1.0)
randint / randrangerandint is INCLUSIVE on both ends; randrange is not
choice / choiceschoices(pop, weights=, k=) samples WITH replacement
samplesample(pop, k) without replacement; counts= for a multiset
shufflein place, on a mutable sequence
uniform gauss normalvariateand expovariate, lognormvariate, betavariate
random.seedreproducible sequences; Random(seed) for an independent stream
random.Randoman instance — the module-level functions share one global instance
secretsthe CSPRNG: token_hex, token_urlsafe, choice, randbelow
Mersenne Twisterrandom is MT19937 — fast, uniform, and NOT cryptographic

decimal, fractions & numeric ABCs

19

decimal

DecimalALWAYS construct from a str or an int, never a float
getcontext.prec (significant digits), .rounding, .traps, .flags
localcontexta context manager for a scoped precision change
quantizeround to a fixed exponent — how you get exactly 2 decimal places
ROUND_HALF_UPand ROUND_HALF_EVEN (the default), ROUND_DOWN, ROUND_CEILING
Decimal.is_nanand is_infinite, is_signed, .as_tuple(), .as_integer_ratio()
InvalidOperationthe trap that fires instead of returning NaN
IEEE 754-2008decimal implements the decimal arithmetic standard, not binary floats

fractions

FractionFraction(1, 3), Fraction("0.25"), Fraction(0.1) (exact, and ugly)
limit_denominatorthe best rational approximation — Fraction(math.pi).limit_denominator(1000)
numerator denominatoralways in lowest terms, denominator positive
from_float / from_decimalexplicit constructors

numbers & conversion

numbers.Numberthe ABC tower: Number > Complex > Real > Rational > Integral
isinstance(x, numbers.Integral)accepts int and bool; NumPy ints register too
float.as_integer_ratiothe exact value a float holds; int and Decimal have it too
float.hex / fromhexexact round-trip through a hexadecimal literal
int.bit_lengthbits needed; bit_count() is popcount (3.10+)
int.to_bytesto_bytes(4, "big", signed=False); from_bytes is the inverse
sys.set_int_max_str_digitsraise the 4300-digit int/str conversion limit (3.11+)

datetime, time & zoneinfo

32

datetime

datetime.nownow(tz) — pass a tz or you get a naive local time
datetime.now(UTC)the correct replacement for the deprecated utcnow()
date.todayand date(y, m, d), datetime(y, m, d, h, mi, s, us, tzinfo)
fromisoformat3.11+ parses the full ISO 8601 grammar, including Z
isoformatthe round-trip partner; sep= and timespec=
strptime / strftimeparse / format; %Y %m %d %H %M %S %f %z %Z %j %A %B %p
timedeltaweeks days hours minutes seconds; .total_seconds()
timestamp / fromtimestampPOSIX seconds; pass tz= to fromtimestamp
astimezoneconvert an aware datetime to another zone
replace(tzinfo=...)ATTACH a zone — it does not convert
combine / date() / time()split and rejoin dates and times
datetime.UTC3.11+ alias for timezone.utc
MINYEAR / MAXYEAR1 to 9999 — no dates before the Common Era

zoneinfo & calendar

ZoneInfoZoneInfo("Europe/London") — the IANA database, 3.9+
available_timezonesthe whole set; tzdata on PyPI supplies it on Windows
fold0 or 1: which of a repeated wall-clock hour you mean, at a DST fallback
calendar.monthrange(weekday of the 1st, number of days)
calendar.isleapand leapdays(y1, y2)
date.isocalendar(ISO year, week, weekday) — week 1 holds the first Thursday
date.weekdayMonday is 0; isoweekday() makes Monday 1

time — clocks

time.timewall clock, POSIX epoch seconds — can jump backwards
time.monotonicnever goes backwards — the right clock for timeouts
time.perf_counterhighest resolution — the right clock for benchmarks
time.process_timeCPU time of this process, excluding sleep
time.thread_timeper-thread CPU time
time.sleepblocks the thread; asyncio.sleep in a coroutine
_ns variantstime_ns, monotonic_ns, perf_counter_ns — integers, no float rounding
time.strftime / gmtimethe C-library layer under datetime

Rules

store UTCconvert to local only at the display boundary
never subtract naive from awareTypeError — and mixing them silently corrupts arithmetic
a day is not 24 hoursDST transitions; add timedelta to a UTC instant, not a local one
monotonic for durationswall-clock deltas break on NTP steps and DST

re — module API

32

Functions

re.compilecompile once when the pattern is reused; the module cache is only 512 deep
re.searchfirst match anywhere
re.matchanchored at position 0 only — not the same as search
re.fullmatchthe entire string must match
re.findallstrings, or tuples if the pattern has groups — a common surprise
re.finditerlazy Match objects; prefer this over findall
re.subsub(p, repl, s, count=0); repl may be a callable taking the Match
re.subnreturns (result, count)
re.splitcapturing groups are included in the result list
re.escapequote a literal for embedding in a pattern
re.purgeclear the compiled-pattern cache

Match objects

m.groupgroup(0) the whole match; group(1, 2) returns a tuple
m[n] / m["name"]__getitem__ shorthand for group()
m.groupsall capture groups; default= fills unmatched optionals
m.groupdictnamed groups only
m.start / m.end / m.spanoffsets, per group
m.expandexpand a template with backreferences
m.lastindex / m.lastgroupwhich group matched last
m.re / m.stringthe pattern and the subject

Flags & constructs

re.IGNORECASE (I)Unicode-aware unless re.ASCII is also set
re.MULTILINE (M)^ and $ match at each line boundary
re.DOTALL (S). also matches a newline
re.VERBOSE (X)whitespace and # comments ignored inside the pattern
re.ASCII (A)make \w \d \s \b ASCII-only
re.UNICODE (U)the default in Python 3; the flag is a no-op
(?i) (?m) (?s) (?x)inline flags — must sit at the start of the pattern
(?:...) (?P<n>...)non-capturing, named
(?=...) (?!...)lookahead, positive and negative
(?<=...) (?<!...)lookbehind — fixed width only in Python
(?>...) and *+ ++ ?+3.11+ atomic groups and possessive quantifiers — kill backtracking
(?(id)yes|no)conditional on whether a group matched
regex (PyPI)variable-width lookbehind, \p{...} properties, fuzzy matching

Serialisation: json, csv, toml, pickle

27

json

json.loads / loadstr or file -> Python; object_hook and object_pairs_hook customise
json.dumps / dumpindent=2, sort_keys=True for a diffable file
ensure_ascii=Falseemit real UTF-8 instead of \uXXXX escapes
default=a callable for objects json cannot encode; default=str is the cheap fix
cls=JSONEncodersubclass and override .default() for a reusable encoder
parse_float=Decimalstop binary floats entering financial data
separators=(",", ":")the compact form — no whitespace
JSONDecodeErrora ValueError subclass; .lineno .colno .pos
type mappingtuple -> array, all keys -> strings; no set, bytes, date or NaN in strict JSON
orjson / msgspecmuch faster, and they serialise dataclasses and datetimes natively

csv

csv.readerrows as lists of strings — everything is a string
csv.DictReaderrows as dicts keyed by the header; fieldnames= to override
csv.writer / DictWriterwriterow, writerows, writeheader
newline=""REQUIRED in the open() call, both reading and writing
csv.Snifferguess the dialect and whether there is a header
delimiter / quotechardialect options; QUOTE_MINIMAL, QUOTE_ALL, QUOTE_NONNUMERIC
csv.field_size_limitraise it for pathological single fields

toml, ini, pickle

tomllib.load3.11+, READ-ONLY, and the file must be opened in binary mode
tomli_w / tomlkitwriting TOML is third-party; tomlkit preserves comments
configparserINI; interpolation, DEFAULT section, read_dict, read_string
pickle.dumps / loadsarbitrary objects; protocol 5 (PEP 574) supports out-of-band buffers
pickle securityunpickling executes __reduce__ — treat it as running the sender code
__getstate__ / __setstate__control what is serialised; __reduce__ for the general case
copyregregister a reducer for a type you do not own
shelvea persistent dict backed by pickle and dbm
marshalCPython internal (.pyc) format — version-specific, not for your data
plistlibApple property lists, binary and XML

sqlite3

23

Connection & cursor

sqlite3.connectconnect(path or ":memory:", timeout=5.0, isolation_level=None)
conn.executea shortcut that creates and returns a cursor
cur.executeexecute(sql, params) — never format SQL with f-strings
placeholders? positional, :name with a dict — the only injection-safe way
cur.executemanyone prepared statement, many parameter rows — far faster
cur.executescriptseveral statements; it COMMITs first
fetchone / fetchmany / fetchallor just iterate the cursor, which is lazy
cur.lastrowidand cur.rowcount, cur.description
conn.commit / rollbackthe connection is a context manager for the TRANSACTION, not the file
conn.closethe with block does not close it — close explicitly

Types & behaviour

sqlite3.Rowconn.row_factory = sqlite3.Row gives index- and name-access rows
row_factoryor any callable(cursor, row); 3.12+ ships a dict factory recipe
detect_typesPARSE_DECLTYPES / PARSE_COLNAMES for date and timestamp adapters
register_adapterPython -> SQLite; register_converter for the way back
conn.create_functioncall a Python function from SQL; deterministic=True to allow indexing
conn.create_aggregateand create_window_function (3.11+), create_collation
autocommit attribute3.12+ — PEP 249 compliant transaction control
isolation_level=Noneautocommit; issue BEGIN yourself for explicit transactions
conn.backuponline backup to another connection, live
conn.iterdumpthe whole database as SQL text
PRAGMA journal_mode=WALconcurrent readers with one writer — set it once per database
PRAGMA foreign_keys=ONOFF by default, per connection
python -m sqlite3 db3.12+ — a REPL shell in the stdlib

typing

35

Core

Anydisables checking in both directions — it propagates
object"anything", but must be narrowed before use — the sound alternative to Any
Never / NoReturnthe empty type; a function that always raises or exits
Optional[X]the old spelling of X | None
Union[X, Y]the old spelling of X | Y (PEP 604, 3.10+)
LiteralLiteral["r", "w"] — a finite set of singleton values
LiteralString3.11+ — a string built only from literals; for SQL and shell APIs
Final / ClassVarnever rebound; a class attribute rather than a dataclass field
AnnotatedAnnotated[int, Meta(...)] — types plus metadata; how pydantic and FastAPI work
TypeAlias / type X = ...the 3.12 type statement makes aliases lazy and explicit
Self3.11+ — the right return annotation for fluent and copy methods
TYPE_CHECKINGFalse at runtime — the standard cure for import cycles

Callables, generics, variance

Callable[[int, str], bool]Callable[..., R] for any signature
ParamSpecPEP 612 — preserve a wrapped function signature through a decorator
Concatenateadd or remove leading parameters alongside a ParamSpec
TypeVarbound= constrains, and 3.12 adds infer_variance=
TypeVarTuple / UnpackPEP 646 variadic generics — shaped array types
def f[T](x: T) -> T3.12+ PEP 695 syntax; the TypeVar is implicit and scoped
class Box[T]:the same for classes; class Box[T: Sized] for a bound
Generic[T]the pre-3.12 base class
covarianceSequence[T] is covariant, list[T] invariant — why list[int] is not list[float]

Structural & narrowing

Protocolstructural subtyping — "has these members", no inheritance required
runtime_checkablelets isinstance() check a Protocol, by members only, not signatures
TypedDicta dict with a fixed key schema; total=False, Required, NotRequired
NamedTuplea typed tuple subclass
NewTypeNewType("UserId", int) — a distinct type with zero runtime cost
TypeGuardPEP 647 — narrow to the given type on True
TypeIs3.13+ PEP 742 — narrows in BOTH branches; almost always the better choice
assert_type / assert_neverchecker assertions; assert_never gives exhaustiveness checking
casttell the checker; no runtime effect and no validation
overloadseveral signatures for one implementation, stub-style
override3.12+ PEP 698 — error if it does not actually override anything
get_type_hintsresolve string annotations; include_extras= keeps Annotated metadata
annotationlib3.14 — Format.VALUE / FORWARDREF / STRING under PEP 649
typing_extensionsevery new typing feature, backported to older Pythons

dataclasses, enum, abc

29

dataclasses

@dataclassinit, repr, eq, order, frozen, slots (3.10+), kw_only (3.10+), match_args
fielddefault, default_factory, init, repr, compare, hash, metadata, kw_only
default_factorythe required idiom for any mutable default
__post_init__validation and derived fields; InitVar passes init-only arguments
fields()the Field descriptors — name, type, default, metadata
asdict / astuplerecursive; dict_factory= to control the container
replacea copy with some fields changed — the functional update
frozen=Trueimmutable and hashable; __setattr__ raises FrozenInstanceError
slots=Truerebuilds the class with __slots__ — it returns a NEW class object
KW_ONLYthe _: KW_ONLY sentinel makes everything after it keyword-only
attrs / pydanticattrs is the older, richer original; pydantic validates and coerces

enum

Enummembers are singletons; identity comparison is correct and fastest
IntEnum / StrEnumalso a real int / str; StrEnum is 3.11+
Flag / IntFlagbitwise combinable; boundary= controls out-of-range bits (3.11+)
auto()successive values; override _generate_next_value_ to change the rule
@uniquereject aliases
Enum.name / .valueand Color["RED"] by name, Color(1) by value
_missing_classmethod hook for unknown values — how you build a lenient enum
@member / @nonmember3.11+ — force or exclude something from membership
EnumMeta / EnumTypethe metaclass; __members__ includes aliases
@verify(CONTINUOUS)3.11+ — assert value invariants at class creation

abc & protocols

abc.ABCinherit from it, or set metaclass=ABCMeta
@abstractmethodinstantiation fails while any abstract method remains
stacking@property over @abstractmethod, in that order
ABCMeta.registervirtual subclass: isinstance passes with no inheritance
__subclasshook__customise issubclass — how collections.abc does duck typing
collections.abcIterable Container Sized Hashable Sequence MutableMapping Set
Mixin methodsinherit Sequence and __getitem__+__len__ gives you the rest free
typing.Protocolthe static alternative — no registration, no base class

contextlib & copy

22

contextlib

@contextmanagerone yield, wrapped in try/finally — the finally is not optional
@asynccontextmanagerthe async with equivalent
suppresswith suppress(FileNotFoundError): — try/except/pass, legibly
closingcall .close() on anything at block exit
aclosing3.10+ — the async twin; use it around async generators
ExitStacka dynamic number of managers; .callback(), .pop_all() to transfer them
AsyncExitStackthe async version
nullcontextnullcontext(x) — a do-nothing manager for optional resources
redirect_stdoutand redirect_stderr — capture prints from code you do not own
chdir3.11+ — process-global and therefore NOT thread-safe
ContextDecoratora manager that also works as a decorator
AbstractContextManagerthe ABC; __exit__ returning truthy SWALLOWS the exception

copy & identity

copy.copyshallow: a new container, the same elements
copy.deepcopyrecursive, memoised, cycle-safe — and slow
memo dictdeepcopy(x, memo) — how the cycle handling is threaded through
__copy__ / __deepcopy__override the behaviour for your own type
copy.replace3.13+ — the generic form of dataclasses.replace, via __replace__
pickle round-tripthe cheap deep copy for plain data; loses identity sharing

contextvars

ContextVarper-task state that survives await, unlike threading.local
var.set / var.getset returns a Token; var.reset(token) restores the old value
copy_contextctx.run(fn) — how asyncio isolates each task
use caserequest IDs and trace context in async servers

asyncio

32

Entry points

asyncio.runcreates the loop, runs the coroutine, closes it. Once, at the top
asyncio.Runner3.11+ — several run() calls sharing one loop and context
python -m asyncioan async REPL where you can await at the prompt
get_running_loopinside a coroutine; get_event_loop is deprecated outside one
loop.run_in_executoroff-load blocking work to a thread or process pool
asyncio.to_thread3.9+ — the ergonomic wrapper for the same thing

Tasks & groups

create_taskschedules a coroutine; keep a reference or it may be collected
TaskGroup3.11+ structured concurrency — waits, cancels siblings, raises a group
gatherresults in argument order; return_exceptions=True collects failures
as_completedyields futures in completion order
waitwait(aws, return_when=FIRST_COMPLETED) -> (done, pending)
wait_fora timeout around one awaitable; cancels it and raises TimeoutError
timeout / timeout_at3.11+ — a deadline around a whole block
shieldprotect an awaitable from outer cancellation
sleepsleep(0) yields to the loop exactly once
current_task / all_tasksintrospection; task.get_name(), set_name()
task.cancelraises CancelledError at the next await point
CancelledErrora BaseException since 3.8 — catch it only to clean up, then re-raise
uncancel3.11+ — the machinery TaskGroup uses to nest cancel scopes
eager_task_factory3.12+ — run a coroutine synchronously until its first suspension

Primitives & I/O

asyncio.Lockand Event, Condition, Semaphore, BoundedSemaphore — NOT threading ones
asyncio.Queueand LifoQueue, PriorityQueue; .join() and .task_done()
open_connectionthe streams API: (reader, writer)
start_servera TCP server from a callback taking (reader, writer)
create_subprocess_execnon-blocking subprocesses; .communicate() is a coroutine
loop.add_signal_handlerPOSIX signals delivered safely on the loop
run_coroutine_threadsafeschedule onto a loop from another thread
call_soon_threadsafethe only loop method safe to call from another thread
debug modeasyncio.run(main(), debug=True) or PYTHONASYNCIODEBUG=1 — finds slow callbacks
uvloopa libuv event loop, several times faster
anyio / triostructured concurrency with cancel scopes and nurseries
httpx / aiohttpasync HTTP clients; requests is blocking and always will be

threading, multiprocessing, futures

30

concurrent.futures

ThreadPoolExecutorI/O bound; max_workers defaults to min(32, cpu+4)
ProcessPoolExecutorCPU bound; arguments and results must be picklable
executor.submitreturns a Future immediately
executor.maplazy, in order; chunksize= matters a lot for processes
as_completediterate futures as they finish, with an optional timeout
future.resultblocks, and RE-RAISES the worker exception here
future.exceptioninspect without raising; .cancel(), .done(), .add_done_callback()
InterpreterPoolExecutor3.14+ — subinterpreter workers, parallel without pickling everything
shutdown(wait, cancel_futures)the with block calls shutdown(wait=True)

threading

ThreadThread(target=, args=, daemon=); .start(), .join(timeout)
Lock / RLockRLock is re-entrant by the owning thread
Event.set() .clear() .wait(timeout) — the simplest signal
Conditionwait / notify / notify_all over a lock — always loop on the predicate
Semaphorebound the concurrency; BoundedSemaphore catches extra releases
Barriern threads rendezvous
threading.localper-thread attribute storage
Timera Thread that fires after a delay
current_thread / enumerateintrospection; .native_id since 3.8
GIL semanticsswitches every sys.setswitchinterval() seconds; += is NOT atomic
daemon threadskilled abruptly at exit — no finally, no atexit

multiprocessing & interpreters

Processthe same API shape as Thread, with a real address space
Poolmap, imap, imap_unordered, apply_async, starmap
get_context("spawn")spawn is the default on macOS and Windows; fork is unsafe with threads
__main__ guardMANDATORY with spawn — the child re-imports your module
Queue / PipeIPC; objects cross by pickling
Value / Arrayshared ctypes memory with a lock
shared_memory3.8+ — a named block; SharedMemoryManager cleans it up
Managerproxied dict/list/Namespace — convenient, and slow
concurrent.interpreters3.14+ PEP 734 — one GIL each, one process, real parallelism
free-threaded buildPEP 703; python3.14t, sys._is_gil_enabled() is False

Testing, Logging & Debugging

45

pytest (PyPI, the default)

assertplain asserts, rewritten to show the operand values
pytest.raiseswith pytest.raises(ValueError, match="regex"): — match is a SEARCH
pytest.approxfloat comparison with rel and abs tolerances; works on collections
@pytest.mark.parametrizea table of cases; ids= to name them
@pytest.fixturescope="function|class|module|session"; yield for teardown
conftest.pyfixtures and hooks shared by a directory tree
tmp_path / monkeypatchbuilt-in fixtures; also capsys, caplog, recwarn, request
pytest -k / -mselect by name expression / by marker
pytest --lf --ff -xlast-failed, failed-first, stop at the first failure
pytest-cov / xdistcoverage; -n auto for parallel test processes
hypothesisproperty-based testing with shrinking counterexamples

unittest & mock

TestCasesetUp, tearDown, setUpClass, addCleanup
assertEqualassertAlmostEqual, assertIn, assertIsNone, assertCountEqual
assertRaisesas a context manager, with .exception afterwards
subTestwith self.subTest(i=i): — keep going after a failure
@skipIf / expectedFailureconditional skipping
IsolatedAsyncioTestCaseasync def test_ methods
mock.patchpatch WHERE THE NAME IS LOOKED UP, not where it is defined
MagicMockreturn_value, side_effect (value, exception or iterable), spec=
assert_called_once_withand call_args, call_count, mock_calls
patch.object / patch.dictattributes and mappings; autospec=True catches bad calls
doctestdoctest.testmod(); pytest --doctest-modules

logging

getLogger(__name__)per module, always; loggers form a dotted hierarchy
basicConfigapplication entry point only; force=True to re-configure
levelsNOTSET 0, DEBUG 10, INFO 20, WARNING 30 (default), ERROR 40, CRITICAL 50
lazy % formattinglog.info("x=%s", x) — the string is not built unless it is emitted
log.exceptionERROR plus the current traceback; inside an except block only
stacklevel= / exc_info=report the caller instead; attach a traceback anywhere
Handler / FormatterStreamHandler, FileHandler, RotatingFileHandler, QueueHandler
dictConfigthe configuration format that is actually maintainable
propagaterecords rise to ancestor loggers — the usual cause of duplicate lines
LoggerAdapter / extra=attach structured context to every record
logging.captureWarningsroute the warnings module into logging

Debug & measure

breakpoint()honours PYTHONBREAKPOINT; =0 disables every breakpoint
pdb commandsn s c r q, l ll w u d, b/tbreak, p/pp, display, interact
pdb.post_mortemdebug the traceback you just caught
python -m pdb -c continuerun to the crash, then land in the debugger
PEP 7683.14 — safe external debugger attach to a running process
timeitpython -m timeit -s setup stmt; timeit.repeat gives you the distribution
cProfile / pstatspython -m cProfile -o out.prof; pstats sort by cumtime
sys.monitoring3.12+ — the low-overhead events API profilers should target
faulthandlerdump a traceback on SIGSEGV, SIGABRT or a timeout
warnings.warnwarn(msg, DeprecationWarning, stacklevel=2) — point at the caller
warnings.deprecated3.13+ — the decorator form, visible to type checkers
ruff / mypylint+format and static types; the two that pay for themselves

Hashing, Crypto & Identity

25

hashlib & hmac

hashlib.sha256and sha1, sha512, sha3_256, blake2b, blake2s
h.update / h.digestand .hexdigest(), .digest_size, .block_size
hashlib.file_digest3.11+ — hash a file object without reading it into memory
hashlib.md5checksums only; usedforsecurity=False on FIPS builds
pbkdf2_hmacpassword hashing with a salt and an iteration count
hashlib.scryptmemory-hard KDF; argon2-cffi on PyPI is the current recommendation
hmac.new / hmac.digestkeyed authentication over a message
hmac.compare_digestconstant-time comparison — never use == on secrets
blake2b(key=...)keyed hashing, salt and person parameters, fast

secrets, uuid, base64

secrets.token_bytesand token_hex(n), token_urlsafe(n) — CSPRNG
secrets.choiceand randbelow(n) — unbiased, cryptographically strong
uuid.uuid4random; uuid1 embeds the MAC and time, uuid5 is a namespaced SHA-1
uuid.UUIDUUID(hex) / .hex .bytes .int .version
base64.b64encodebytes in, bytes out — decode() if you want a str
urlsafe_b64encode- and _ instead of + and /
b32 / b16 / b85encodethe other alphabets; b85 for compact binary in text
binascii.crc32zlib.crc32 too — a checksum, not a hash

ssl & hazards

ssl.create_default_contextthe only correct starting point — verifies and checks hostnames
context.load_verify_locationsa custom CA bundle; certifi supplies one on PyPI
check_hostname / verify_modedisabling these is how TLS gets silently broken
SSLContext.wrap_socketserver_hostname= is required for SNI
ssl.SSLCertVerificationErrorthe exception you get for expired or untrusted chains
cryptographythe library for actual crypto — Fernet, X.509, AEAD, key derivation
PyNaCl / argon2-cffilibsodium bindings; password hashing
do not roll your ownno home-made ciphers, no ECB, no static IVs, no == on MACs

Networking & the Web

31

urllib & http

urllib.request.urlopenstdlib HTTP; a context manager returning a file-like response
urllib.parse.urlparsescheme, netloc, path, params, query, fragment
urlencode / parse_qsbuild and parse query strings; quote and unquote for components
urljoinresolve a relative URL against a base — do not concatenate strings
http.clientthe low-level HTTP/1.1 protocol implementation
http.serverpython -m http.server 8000 — development only, never exposed
http.cookies / cookiejarcookie parsing and a client-side jar
HTTPStatushttp.HTTPStatus.NOT_FOUND == 404, with .phrase
requests / httpxwhat everyone actually uses; httpx also does async and HTTP/2

socket & addresses

socket.socketAF_INET / AF_INET6 / AF_UNIX, SOCK_STREAM / SOCK_DGRAM
bind listen accept connectthe Berkeley sequence, unchanged
send / recvrecv returns AT MOST n bytes; sendall loops for you
settimeout / setblockingtimeouts raise socket.timeout (an alias of TimeoutError)
setsockopt SO_REUSEADDRthe fix for "address already in use" after a restart
getaddrinfothe correct, family-agnostic name resolution
socketserverThreadingTCPServer and friends — small servers, quickly
selectorsthe portable readiness API (epoll/kqueue) under asyncio
ipaddressIPv4Address, IPv6Network; .subnets(), in-network containment tests
socket.if_nametoindexinterface indices for scoped IPv6

Formats & protocols

email.message.EmailMessagethe modern API; .set_content(), .add_alternative()
email.parser / policy.defaultparse MIME correctly — the legacy policy is a trap
smtplib / imaplib / poplibsending and fetching mail
mimetypes.guess_typeextension -> content type
html.escape / unescapequote=True escapes quotes too
html.parser.HTMLParseran SAX-style parser in the stdlib
xml.etree.ElementTreefind, findall, iterfind with a small XPath subset
defusedxmlPyPI — stdlib XML is vulnerable to entity-expansion attacks
beautifulsoup4 / lxmlreal HTML parsing; lxml also gives full XPath
webbrowser.openhand a URL to the desktop browser
wsgirefthe reference WSGI server and validator
flask / fastapi / djangothe three you will meet; fastapi builds its API from type hints

Text & Data Utilities

30

string & text

string.ascii_lettersand ascii_lowercase, digits, punctuation, whitespace, printable
string.Template$name / ${name}; .substitute raises, .safe_substitute does not
string.capwordstitle-case, splitting on whitespace only
textwrap.fill / wrapwidth=, break_long_words=, subsequent_indent=
textwrap.dedentstrip the common leading whitespace — for triple-quoted blocks
textwrap.shortencollapse and truncate with a placeholder
textwrap.indentprefix every line, with an optional predicate
shlex.splitPOSIX word splitting; shlex.quote to build a safe shell string
shlex.join3.8+ — the inverse; use it instead of " ".join
difflib.unified_diffand ndiff, HtmlDiff, SequenceMatcher.ratio()
difflib.get_close_matchesfuzzy suggestions — the "did you mean" primitive
unicodedataname, category, normalize (NFC NFD NFKC NFKD), combining, east_asian_width
codecslookup, register, incremental encoders; codecs.BOM_UTF8
localesetlocale, getpreferredencoding; format_string for locale-aware numbers
gettexti18n message catalogues, the _() convention

Structure & display

pprint.pp3.8+ — sort_dicts=False by default, unlike pprint()
reprlib.Reprconfigurable truncation limits per type
json.dumps(indent=2)often the most readable dump of plain data
dataclasses.asdicta nested structure you can pprint or serialise
csv + io.StringIOrender a table to a string without touching disk
zoneinfo + f"{dt:%c}"strftime codes work directly in a format spec
rich / tabulatetables, syntax highlighting and progress bars in a terminal

Archives & compression

gzip.opena drop-in for open() with mode "rt"/"wb"
bz2 / lzmathe same interface; lzma is xz — smallest and slowest
zlib.compressraw deflate; crc32 and adler32 checksums
zipfile.ZipFilenamelist, read, extractall, writestr; ZIP_DEFLATED must be asked for
zipfile.Path3.8+ — a pathlib-style view inside an archive
tarfile.openmode "r:gz", "w:xz"; filter="data" is the safe default since 3.12
path traversalan archive can hold ../ and absolute members — always filter or validate
shutil.make_archivethe one-line wrapper over both

inspect, importlib & the C Boundary

28

inspect

inspect.signaturea Signature; .bind(*a, **kw), .parameters, .return_annotation
Parameter.kindPOSITIONAL_ONLY, POSITIONAL_OR_KEYWORD, VAR_POSITIONAL, KEYWORD_ONLY, VAR_KEYWORD
inspect.getsourceand getsourcefile, getsourcelines, getdoc, cleandoc
inspect.getmemberswith a predicate: isfunction, isclass, ismethod, isgenerator
inspect.iscoroutinefunctionand isasyncgenfunction, isawaitable
inspect.stack / currentframethe call stack as FrameInfo records — expensive
inspect.getmrothe C3 linearisation, as a tuple
inspect.unwrapfollow __wrapped__ back through decorators
inspect.getclosurevarswhat a function actually captured

importlib

import_moduleimport by a name computed at runtime
importlib.reloadre-executes the module; existing references keep the old objects
importlib.metadataversion(), distributions(), entry_points() — no pkg_resources
importlib.resources.filesread package data portably, even from inside a wheel or zip
importlib.util.find_specis it importable, and from where — without importing it
sys.meta_paththe finder chain; how import hooks and lazy loaders are installed
MetaPathFinder / Loaderthe two-step protocol: find_spec then exec_module
LazyLoaderdefer execution until the first attribute access
__getattr__ (module)PEP 562 — module-level lazy attributes and deprecation shims
namespace packagesPEP 420 — no __init__.py, and portions may span sys.path entries

C interop & extension

ctypes.CDLLload a shared library; .argtypes and .restype are not optional
ctypes.Structurec_int, c_char_p, POINTER, byref, sizeof, _fields_, _pack_
struct vs ctypesstruct for wire formats, ctypes for calling into a library
memoryview / buffer protocolzero-copy hand-off to C, NumPy and PEP 688
PYTHONMALLOC=debugcatch buffer overruns in extension code
cffideclare C from its own headers — usually preferable to ctypes
Cython / pybind11 / nanobindcompiled extensions; nanobind is the lean modern C++ binding
PyO3 / maturinwriting the extension in Rust, and packaging it as a wheel
limited API / abi3one wheel across versions; PEP 703 needs opt-in for free-threading

The Standard Library, by Domain

44

Language & runtime

builtins typesthe built-in namespace; types.SimpleNamespace, FunctionType, MappingProxyType
sys sysconfig platforminterpreter, build configuration, host
gc weakref tracemallocmemory: cycles, non-owning references, allocation tracing
abc numbersabstract base classes; the numeric tower
typing annotationlibannotations; annotationlib is 3.14
dataclasses enumrecords and enumerations
contextlib contextvarsthe with protocol; async-safe task-local state
copy copyreg pickleduplication and serialisation
inspect dis ast symtableintrospection, bytecode, syntax trees, scopes
importlib pkgutil runpy zipimportthe import system, python -m, imports from a zip
warnings traceback linecachediagnostics
atexit signal faulthandlershutdown, signals, crash tracebacks
keyword token tokenize opcodethe lexical layer
site venv ensurepip zipappenvironments and single-file applications

Data & algorithms

collections heapq bisect arraycontainers, heaps, sorted lists, packed numbers
itertools functools operatoriteration, higher-order functions, operators as functions
graphlibtopological sorting
math cmath statistics random secretsnumerics and randomness
decimal fractionsexact decimal and rational arithmetic
datetime time calendar zoneinfothe temporal stack
re difflibpattern matching and sequence diffing
string textwrap unicodedata codecstext
struct mmapbinary layouts and memory-mapped files
json csv tomllib configparser plistlibdata formats
sqlite3 dbm shelveembedded storage
xml html email mimetypesmarkup and messages
base64 binascii quopribinary-to-text encodings
hashlib hmac zlibdigests and checksums

System, I/O & network

os io pathlib statfiles and the process environment
shutil glob fnmatch fileinput filecmp tempfilefilesystem utilities
gzip bz2 lzma zipfile tarfile compressioncompression and archives
subprocess shlex getpass pty tty termiosprocesses and terminals
threading multiprocessing queue schedconcurrency
concurrent.futures concurrent.interpreterspools; subinterpreters (3.14)
asyncio selectors selectthe event loop and readiness APIs
socket socketserver ssl ipaddresssockets and TLS
urllib http ftplib smtplib imaplib poplibthe protocol clients
wsgiref xmlrpc webbrowser netrc uuidweb plumbing and identity
logging argparse getopt optparselogging and command lines (optparse is legacy)
curses tkinter turtle colorsys waveterminals, GUI, graphics, audio
unittest doctest pdb bdbtesting and debugging
timeit cProfile profile pstats tracemeasurement
pydoc pyclbr tabnanny compileall py_compiledocumentation and build tooling
ctypes mmap fcntl resource syslog grp pwdthe OS boundary (several are POSIX-only)

Command Line & Tooling

33

python

python -m mod argsrun an installed module as __main__ — venv, pip, pytest, http.server
python -c "code"one-off statements
python -i script.pyrun, then land in the REPL with its globals
python -O / -OOstrip asserts and __debug__ blocks; -OO also strips docstrings
python -X devdevelopment mode: extra checks and default warning filters
python -X importtimeper-import cost — the first tool for slow start-up
python -X faulthandlertraceback on a fatal signal
python -X gil=0free-threaded build: run with the GIL off
python -W error::DeprecationWarningturn one warning class into an exception
python -P / -I / -E / -sdo not add the script dir; isolated; ignore env; ignore user site
python -uunbuffered stdio — the fix for missing logs in a pipe
python -Bno __pycache__ writes
python -m sitewhere site-packages actually is
python -m json.toolpretty-print JSON from a pipe; -m sqlite3, -m http.server, -m venv

Environments & packaging

python -m venv .venv--upgrade-deps, --system-site-packages, --prompt
python -m pip install -e .editable install of the project in the working directory
python -m pip install -r req.txtand pip freeze, pip list --outdated, pip show, pip check
PIP_REQUIRE_VIRTUALENV=1refuse to install into the system interpreter
pyproject.toml[project], [build-system], [project.scripts], [tool.*] — one file
requires-pythonpip will refuse to install where the package cannot run
uvuv venv / uv sync / uv run / uv add / uv python install — a Rust reimplementation
uv.lock / pip-toolsreproducible resolution; pip freeze is not a lock file
pipxinstall CLI tools into isolated environments (uv tool install does the same)
hatchling / setuptools / flitbuild backends; hatchling is the common modern default
build / twine / cibuildwheelpython -m build, upload to PyPI, wheels for every platform
tox / noxrun the test suite across interpreter versions

Quality

ruff check --fixlint and autofix; ruff format replaces black
mypy --strictthe gradual type checker; pyright for editor-grade speed
pytest --covtests and coverage
pre-commitrun the lot on every commit
bandit / pip-auditsecurity lint; known vulnerabilities in your dependencies
py-spy / scalene / memraysampling profiler, CPU+GPU+memory profiler, allocation tracer
ipython / jupytera better REPL; notebooks

Removed, Deprecated & Renamed

30

Removed

distutilsremoved 3.12 (PEP 632) — setuptools or hatchling with pyproject.toml
impremoved 3.12 — importlib
asynchat asyncore smtpdremoved 3.12 — asyncio, or aiosmtpd
the dead batteriesremoved 3.13 (PEP 594): aifc audioop cgi cgitb chunk crypt imghdr
… continuedmailcap msilib nis nntplib ossaudiodev pipes sndhdr spwd sunau telnetlib uu xdrlib
unittest camelCase aliasesremoved 3.12: assertEquals, failUnless, makeSuite, getTestCaseNames
locale.resetlocaleremoved 3.13; also removed: turtle.RawTurtle.settiltangle
typing.io / typing.reremoved 3.13 — the names live in typing itself
lib2to3 / 2to3removed 3.13

Deprecated

datetime.utcnowdeprecated 3.12 — datetime.now(datetime.UTC); utcfromtimestamp too
typing.List Dict Tuple Setdeprecated since 3.9 — use list, dict, tuple, set
typing.Union / Optionalnot removed, but X | Y and X | None read better (3.10+)
locale.getdefaultlocaledeprecated 3.11 — getlocale(), getencoding()
ssl.wrap_socketremoved 3.12 — SSLContext.wrap_socket
importlib.resources.read_textlegacy API — use files() / as_file()
pkg_resourcessetuptools legacy — importlib.metadata and importlib.resources
asyncio.get_event_loopdeprecated outside a running loop — asyncio.run or get_running_loop
asyncio.coroutine / @asyncio.coroutineremoved 3.11 — async def
unittest.IsolatedAsyncioTestCase.setUpsync setUp on an async case is deprecated
os.getcwdb / os.path.walklong gone; use os.getcwd() and os.walk
from __future__ import annotationsunnecessary in 3.14 — PEP 649 makes annotations lazy

Changed behaviour worth knowing

PEP 667 locals()3.13 — locals() at function scope is an independent snapshot
PEP 688 buffer protocol3.12 — __buffer__ / __release_buffer__ in pure Python
PEP 684 per-interpreter GIL3.12 — the groundwork for concurrent.interpreters
PEP 758 except without parens3.14 — except ValueError, TypeError: is legal again
int/str conversion limit3.11 — 4300 digits by default; sys.set_int_max_str_digits
zip strict=3.10 — opt in and unequal lengths stop being silent
Enum __str__3.11 — str(Color.RED) is "Color.RED"; the 3.10 f-string mismatch is gone
tarfile filter=3.12 — extraction filters; "data" becomes the default in 3.14
default encoding3.15 — open() will default to UTF-8; pass encoding= now