Python quick reference

471 entries — one card per concept, for looking something up rather than learning it. Each links back to the full explanation.

471

Python Fundamentals — Core language

24

b = a

Binds a second name to the same object — no copy is made. Use list(a), a[:] or a.copy() for a shallow copy, and copy.deepcopy(a) when the object nests.

b = a; b.append(8) # a changed too

int · float · complex

int is exact and unbounded, float is an IEEE-754 double, complex pairs two floats. / returns a float; // floors toward negative infinity.

-7 // 2 # -4

numbersarithmeticfloatbuiltin
Numbers

"".join(parts)

Concatenate an iterable of strings in a single pass. The linear way to build a string, because str is immutable and += rebuilds it every time.

", ".join(["python", "async"]) # 'python, async'

strimmutableperformance
Strings

f"{expr!conv:spec}"

Evaluate expr, optionally convert with !r/!s/!a, then format with spec (width, precision, alignment, type).

f"{total:.2f}" # '59.70'

strformatting
f-strings

str.encode('utf-8') / bytes.decode('utf-8')

Cross the text/binary boundary in either direction. bytes is immutable; bytearray is the mutable form.

"café".encode("utf-8")

bytesbytearrayencodingstdtypes
Bytes and bytearrays

[a, b, c] · list.append(x) · list.sort()

Ordered, mutable sequence. append adds at the end; sort() reorders in place and returns None, sorted() returns a new list.

skus = ["SKU-8841"]; skus.append("SKU-2210")

sequencemutablebuiltin
Lists

(a, b, c) · tuple(iterable)

Immutable sequence. Fixed length, indexed by position, hashable when every element is hashable.

city, latitude, longitude = ("Berlin", 52.52, 13.40)

sequenceimmutablebuiltin
Tuples

set(iterable)

Build an unordered collection of unique, hashable items. Combine with | & - ^; test with in.

sorted(set(monday_visitors) & set(tuesday_visitors))

builtincollectionsdedup
Sets

d = {key: value}

A mapping from hashable keys to values, kept in insertion order. d[key] raises KeyError when the key is absent; d.get(key, default) does not.

settings.get("retry_budget", 3)

dictmappingbuiltinhashable
Dictionaries

seq[start:stop]

New sequence from start up to but not including stop. Omitted bounds default to 0 and len(seq); out-of-range bounds clamp instead of raising.

records[1:4] # positions 1, 2, 3

sequencessyntax
Slicing

seq[start:stop:step]

Take every step-th item from start toward stop. A negative step walks backwards and swaps which end the omitted bounds mean.

readings[::-1]

sequenceslicingsyntax
Extended slicing

host, port = endpoint

Bind several names in one statement by taking an iterable apart position by position. The number of names must equal the number of values, or ValueError.

for resource, amount in limits.items():

assignmenttupleiterablesyntax
Unpacking

first, *rest = items · *leading, last = items · first, *middle, last = items

One starred target per assignment absorbs the leftover items as a list, even when there are none. Plain names are filled first from both ends.

first, *middle, last = line_items

assignmentunpackingsequencepep-3132
Extended iterable unpacking

[expression for item in iterable if condition]

One expression that walks an iterable once and builds a container. The brackets choose the container; the loop name stays inside them.

[item["sku"] for item in line_items if item["paid"]]

comprehensioniterationexpressionscope
Comprehensions

[expr for item in iterable if cond]

Build a new list in one eager pass. The filter if goes last; a conditional expression goes in the output slot at the front.

[amount for amount in line_items if amount > 0]

comprehensionslistssyntax
List comprehensions

{expr for item in iterable}

Build a set from an iterable, dropping duplicates as it goes. Every element produced must be hashable, and the result has no order.

{record["email"].split("@")[1] for record in signups}

comprehensionsetdedup
Set comprehensions

{key_expr: value_expr for item in iterable}

Builds a new dict in one expression. A trailing if filters pairs before they land; a repeated key is silently overwritten by the later pair.

{post_id: slug for slug, post_id in slug_to_id.items()}

dictcomprehensionmappingsyntax
Dictionary comprehensions

(expr for item in iterable)

Produces items on demand instead of building a container. Flat memory, one-shot, and no len(), indexing or slicing.

sum(unit_price * quantity for _, unit_price, quantity in line_items)

generatorslazycomprehensionsmemory
Generator expressions

value_if_true if condition else value_if_false

An expression evaluating to one of two values. else is required, only the chosen branch is evaluated, and it binds looser than arithmetic.

status = "retry" if attempts < retry_budget else "give up"

expressionscontrol-flowsyntax
Conditional expressions

bool(value) · if value:

Every object has a truth value. False, None, the zeros and every empty container are falsy; everything else is truthy. bool() tries __bool__, then __len__, then answers truthy.

display_name = raw_name or "anonymous"

boolcontrol-flowoperators
Truthiness

value is None

None is the sole instance of NoneType — the object meaning "no value". Test for it by identity, and use it as the sentinel default in place of a mutable one.

def add_tag(tag, tags=None): ...

noneidentitysentinelconstants
None

a is b · a == b

is compares identity (same object); == compares value via __eq__. Equal values are not always one object — reserve is for None, True, False and true identity checks.

if cached_row is None: ...

identityequalityoperatorscomparison
is vs ==

hash(obj)

Returns an int used to file obj in a dict or set. Requires equal objects to return equal hashes, which is why every mutable built-in is unhashable — and why defining __eq__ on a class disables its hash until you supply one that agrees.

cache[user_id, tuple(filters)] = result

hashdictsetequality
Hashability

mutable → edits in place · immutable → replaces

A mutable object (list, dict, set) can change without becoming a new object, so every alias sees the edit. An immutable one (int, str, tuple) never changes — every "update" rebinds a name to a new object instead.

tags += ["new"] # mutates if tags is a list, rebinds if tags is a tuple

mutabilityidentityfunctionsside-effects
Mutable vs immutable objects

Python Fundamentals — Functions

17

def name(params):

Builds a function object from the indented block and binds name to it. The body does not run, and its names are not checked, until the function is called.

def greet(name, greeting="Hello"): return f"{greeting}, {name}!"

functionsdefbinding
Defining functions

func(value1, value2, ...)

Arguments matched to parameters by position, in order. A missing or extra argument raises TypeError naming the mismatch; a same-typed swap raises nothing — it just runs with the wrong values in the wrong places.

create_user("nova", "nova@example.com", "admin")

functionsargumentscalling-conventions
Positional arguments

func(name1=value1, name2=value2, ...)

Arguments matched to parameters by name, in any order, after any positional arguments. An unrecognized name or one that duplicates a positional fill raises TypeError naming the problem keyword.

create_user(role="admin", username="nova", email="nova@example.com")

functionsargumentscalling-conventions
Keyword arguments

def func(param=default):

Makes param optional; omitted calls use default, computed once at def time and reused by every such call. Never use a mutable literal as a default — default to None and build the value inside the body instead.

def add_tag(tag, tags=None): if tags is None: tags = []

functionsargumentsmutabilitycommon-mistake
Default arguments

def func(pos, *, kw_only1, kw_only2=default):

Everything after a bare * in the parameter list can only be passed by name. Passing one positionally, or omitting one with no default, is a TypeError.

def resize(image, *, width, height): ... resize("photo.jpg", width=800, height=600)

functionsargumentscalling-conventions
Keyword-only arguments

def name(pos_only1, pos_only2, /, normal, *, kw_only):

A bare / marks every parameter before it as reachable only by position — never by name. Lets a function change a parameter name later without breaking any caller, and matches how several builtins (len, pow) already behave.

def power(base, exp, /): return base ** exp

functionsargumentscalling-conventions
Positional-only arguments

def name(*args):

Collects every extra positional argument into a tuple named args (any name works). The same * unpacks a sequence into separate positional arguments at a call site — two directions, one symbol.

def total(*amounts): return sum(amounts)

functionsargumentsvariadic
*args

def name(**kwargs):

Collects every extra keyword argument into a dict named kwargs (any name works), keyed by the name each was passed with. The same ** unpacks a dict into separate keyword arguments at a call site.

def build_query(**filters): return filters

functionsargumentsvariadic
**kwargs

name = some_function

A function is an ordinary object — assignable, storable in a container, passable as an argument, returnable from another function. A bare name refers to the function object; name() calls it.

loud = shout funcs = [shout, str.lower]

functionsobjectshigher-order
First-class functions

map(f, iterable) · filter(f, iterable) · sorted(items, key=f)

A function that takes a function as an argument, returns one, or both. Built on functions being first-class — a function argument or return value works exactly like any other.

list(filter(lambda n: n > 0, [1, -2, 3, -4, 5]))

functionshigher-orderfunctional
Higher-order functions

def outer(): x = ... def inner(): nonlocal x ... return inner

A nested function that keeps access to variables from its enclosing function even after that function has returned. nonlocal is required to reassign (not just read) a captured variable. Each call to the outer function creates an independent closure.

counter_a = make_counter() counter_a() # 1 counter_a() # 2

functionsscopeclosures
Closures

lambda args: expression

A nameless, single-expression function. The expression's value returns automatically — no return keyword, no statements, no multiple lines. Most useful passed directly as a short callback (sorted's key=, for example) rather than assigned to a name.

sorted(people, key=lambda p: p[1])

functionslambdaanonymous
Lambda functions

def name(param: Type = default) -> ReturnType:

Optional expressions attached to parameters and the return value, stored on func.__annotations__. Read by type checkers and IDEs — never enforced by the interpreter at call time.

def add(a: int, b: int) -> int: return a + b

functionsannotationstyping
Function annotations

func.__name__ · func.__doc__ · func.__dict__

A function built with def is an instance of type function, carrying built-in introspection attributes (__name__, __doc__, __defaults__, __code__) plus its own __dict__ — arbitrary custom attributes can be set on it directly.

greet.call_count = 0 greet.call_count += 1

functionsobjectsintrospection
Function objects

Local → Enclosing → Global → Built-in

The order Python searches when a name is looked up inside a function — the first scope that defines the name wins. Assigning to a name anywhere in a function makes it local for the function's entire body, which is what causes UnboundLocalError when a name is read before its local assignment runs.

x = "global" def f(): x = "local" # shadows the global x, inside f only

functionsscopelegb
Scope and LEGB

global name

Declares that name, for the rest of the function, refers to the module-level variable — assignments update it directly instead of creating a local. Only needed for reassignment; reading a global works without it.

def increment(): global count count += 1

functionsscopeglobal
global

nonlocal name

Declares that name refers to the nearest enclosing function's variable, not a new local — required to reassign a captured variable inside a closure. Reaches exactly one enclosing function scope, never the module (that is global's job).

def increment(): nonlocal count count += 1

functionsscopeclosures
nonlocal

Python Fundamentals — Built-ins worth knowing well

14

enumerate(iterable, start=0)

Wraps an iterable in a lazy sequence of (index, item) tuples, counting from start. Unpack in a for loop instead of tracking a counter by hand.

for i, item in enumerate(items, start=1): print(i, item)

builtinsiterationloops
enumerate

zip(*iterables)

Pairs items from two or more iterables position by position, lazily, stopping at the shortest one. dict(zip(keys, values)) builds a dict from two parallel lists.

list(zip([1, 2, 3], ["a", "b", "c"]))

builtinsiterationloops
zip

sorted(iterable, key=None, reverse=False)

Returns a new sorted list from any iterable, leaving the original unchanged. key names a function to sort by; reverse=True sorts descending. Stable: ties keep their original order.

sorted(words, key=len, reverse=True)

builtinssortingiteration
sorted

reversed(sequence)

Lazy iterator walking a sequence back to front. Needs a real sequence (supports len() and indexing), leaves the original untouched, unlike the in-place list.reverse().

for item in reversed(items): print(item)

builtinsiterationsequences
reversed

any(iterable) · all(iterable)

any() is True if at least one item is truthy; all() is True only if every item is. Both short-circuit. any([]) is False, all([]) is True.

any(n > 10 for n in nums)

builtinsbooleaniteration
any and all

min(iterable, key=None, default=...) · max(iterable, key=None, default=...)

Return the smallest/largest item — or of several separate arguments. key compares a derived value instead of the item itself. Raises ValueError on an empty iterable unless default is given.

max(words, key=len)

builtinsaggregationiteration
min and max

len(container) · sum(iterable, start=0)

len() returns item count for any sequence or collection, O(1). sum() adds a numeric iterable to start, left to right — raises TypeError on strings; use str.join instead.

sum(nums, 100)

builtinsaggregationiteration
sum and len

map(func, iterable, ...)

Applies func to every item lazily, yielding results one at a time. With several iterables, pairs items positionally and stops at the shortest — wrap in list() to materialize.

list(map(str.upper, ["a", "b"]))

builtinsiterationfunctional
map

filter(func, iterable) · filter(None, iterable)

Keeps items where func(item) is truthy, lazily. filter(None, iterable) drops falsy items directly, with no function — including 0, "", and None alike.

list(filter(lambda n: n > 0, nums))

builtinsiterationfunctional
filter

iter(iterable) · next(iterator, default=...)

iter() returns a fresh iterator from an iterable. next() pulls the next value and advances it, raising StopIteration when exhausted unless a default is given. A for loop is this pair, automated.

it = iter([1, 2]); next(it)

builtinsiterationiterators
iter and next

range(stop) · range(start, stop) · range(start, stop, step)

A lazy, immutable sequence of evenly-spaced integers, computed on demand from start/stop/step. stop is always excluded. Supports len(), indexing, and "in" like a real sequence.

list(range(0, 10, 2))

builtinsiterationloops
range

isinstance(obj, type_or_tuple) · issubclass(cls, type_or_tuple)

isinstance checks an object against a type (or tuple of types), including subclasses. issubclass checks a class against a type the same way. bool is a subclass of int.

isinstance(value, (int, float))

builtinstypesintrospection
isinstance and issubclass

getattr(obj, name, default=...) · setattr(obj, name, value) · hasattr(obj, name)

Read/write/check an attribute by name, as a string, rather than dotted syntax — the only way when the name is chosen at runtime. getattr raises AttributeError unless a default is given.

getattr(config, field, "unset")

builtinsintrospectionattributes
hasattr, getattr, and setattr

callable(obj)

True if obj() would be legal — functions, classes, and instances whose type defines __call__. Never calls obj, and says nothing about whether the call would succeed.

callable(list) # True — calling it constructs a new list

builtinsintrospectionfunctions
callable

Python Fundamentals — Modules and imports

8

import name · import name as alias · from name import thing

A module is any .py file. import runs it once and binds a namespace; from ... import binds one name directly.

from mathy import area as circle_area

modulesimports
Modules

package/__init__.py · package.submodule

A directory becomes an importable package once it contains __init__.py. Submodules follow the folder structure, dot by dot.

from shop.utils.formatting import as_currency

modulesimportspackages
Packages

package/__init__.py

Marks a directory as a package; runs once on first import. Re-export a submodule name here to expose it directly through the package.

from .pricing import apply_discount # in shop/__init__.py

modulesimportspackages
__init__.py

from pkg.mod import x · from .mod import x · from ..pkg import x

Absolute imports spell the full path; relative imports count leading dots from the current module — one dot for this package, two for its parent.

from .utils.formatting import as_currency

modulesimportspackages
Absolute and relative imports

sys.path (search order) · sys.modules (import cache)

import searches sys.path in order and stops at the first match; the result is cached in sys.modules, so re-importing a name never re-runs the file.

'counter' in sys.modules

sys.path · sys.path.append(dir) · sys.path.insert(0, dir)

The ordered list import searches. Starts with the running script's directory; append/insert extend it at runtime.

sys.path.append("/extra/modules")

modulesimports
sys.path

if __name__ == "__main__":

__name__ is "__main__" when a file runs directly, or the module's own name when imported. Guard script-only logic with this check.

if __name__ == "__main__": main()

import x (survives) · from x import name (fails, usually)

A imports B, B imports A back. from-imports fail on the not-yet-defined name; plain module imports usually survive because the attribute lookup happens later.

import a def func_b(): a.func_a()

modulesimports
Circular imports

Object-Oriented Python — OOP fundamentals

15

class Name: ... · obj = Name()

A class is a blueprint; calling it builds one independent object (instance).

class Dog: pass rex = Dog()

def __init__(self, arg1, arg2): ...

The constructor's setup step — called automatically right after Name(...) builds the object, to set starting attributes on self.

class Dog: def __init__(self, name): self.name = name

oopclassesdunder
Constructors

self.attr = value

Data that belongs to one instance, stored in that instance's own __dict__ — independent from every other instance of the same class.

rex.age = 3 print(vars(rex))

oopclassesattributes
Instance attributes

class Name: attr = value

Data attached to the class itself, shared by every instance that has not set its own attribute of the same name.

class Dog: count = 0 Dog.count += 1

oopclassesattributes
Class attributes

def method(self, arg): ... → obj.method(arg)

A function defined in a class body that receives the instance it was called through as self, automatically.

class Dog: def bark(self): return f"{self.name} says woof"

oopclassesmethods
Instance methods

@classmethod def name(cls, ...): ...

A method bound to the class (cls), not an instance — most often used to build an alternative constructor that also works for subclasses.

@classmethod def from_string(cls, text): return cls(*text.split("-"))

oopclassesmethods
Class methods

@staticmethod def name(args): ...

A method with no self or cls — an ordinary function kept in the class namespace because it is logically related to the class.

@staticmethod def is_valid_name(name): return bool(name)

oopclassesmethods
Static methods

self._internal = x · self.__hidden = x

Bundling data with the methods that use it; underscore prefixes signal internal-use-only by convention, not by enforced access control.

class Account: def __init__(self, balance): self.__balance = balance

oopclassesconventions
Encapsulation

obj.method() — interface, not implementation

Depending on WHAT a method does rather than HOW it does it, so the internal implementation is free to change without breaking callers.

stack.push(x) # hides whatever storage is used underneath

class Sub(Base): def method(self): return super().method()

A subclass gains every Base attribute and method, can override any of them, and reaches the parent version from inside an override with super().

class Puppy(Dog): def __init__(self, name, weeks_old): super().__init__(name) self.weeks_old = weeks_old

oopclassesinheritance
Inheritance

for obj in objs: obj.method()

Calling the same method name on objects of different types and getting each type's own behaviour — no shared base class required (duck typing).

for shape in [Circle(2), Rectangle(3, 4)]: print(shape.area())

self.part = Part() # created and owned here

A "has-a" relationship where the whole creates and owns the part — the part's lifetime is tied entirely to the owner's.

class Car: def __init__(self): self.engine = Engine()

self.part = part # created elsewhere, referenced here

A "has-a" relationship where the whole references a part it did not create — the part can be shared and outlives the whole.

team = Team("Falcons", [existing_player])

self.related = [] # neither owns the other

The general "knows-about/uses" relationship between two independent objects — no ownership in either direction; aggregation and composition are stricter special cases.

student.courses.append(course) course.students.append(student)

is-a → inherit · has-a / varies independently → compose

Inheritance models a fixed "is-a" hierarchy; composition assembles behaviour from swappable parts. Favour composition once more than one independent dimension of variation exists.

class Duck: def __init__(self, fly_behavior): self.fly_behavior = fly_behavior

Object-Oriented Python — Abstract classes and interfaces

6

from abc import ABC, abstractmethod

ABC is the base class to inherit from; @abstractmethod marks a method every concrete subclass must override before it can be instantiated.

class Shape(ABC): @abstractmethod def area(self): ...

oopabcabstract
The abc module

class Name(ABC): ...

Inheriting from ABC enables @abstractmethod enforcement — the class (and any subclass missing a required method) cannot be instantiated.

class Shape(ABC): @abstractmethod def area(self): ...

oopabcabstract
ABC

@abstractmethod def name(self): ...

Marks a method as required for every concrete subclass — enforced only inside a class using ABCMeta (via ABC). Stacks innermost with @property/@classmethod/@staticmethod.

class Shape(ABC): @abstractmethod def area(self): ...

oopabcabstract
@abstractmethod

obj.method() # no type check, just calls it

Python resolves a method call by whether the object has it, not by its declared class — "if it walks like a duck and quacks like a duck."

def load(source): return source.read() # any object with .read() works

ooptypingduck-typing
Python's duck typing

class Name(Protocol): def method(self) -> T: ...

Names a structural type — any class with matching methods satisfies it, no inheritance required. Add @runtime_checkable for isinstance() support.

@runtime_checkable class Readable(Protocol): def read(self) -> str: ...

ooptypingprotocol
Protocols

compatible ⇔ same shape (structural) vs. compatible ⇔ declared relationship (nominal)

Structural typing judges compatibility by an object's methods/attributes; nominal typing requires an explicit inherits-from/implements declaration. Python defaults to structural.

def send_up(x: FlyerProtocol) -> str: return x.fly() # any matching shape works

ooptypingstructural-typing
Structural typing

Object-Oriented Python — Properties

4

@property def name(self): return ...

Turns a method into an attribute-style read — obj.name, no parentheses. Runs fresh on every read; read-only unless a setter is added.

@property def area(self): return 3.14159 * self.radius ** 2

oopproperty
@property

@name.setter def name(self, value): self._name = value

Makes a property writable — obj.name = value runs this method, most often to validate or transform the value before storing it.

@celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("below absolute zero") self._celsius = value

@name.deleter def name(self): ...

Defines what del obj.name does — reset the underlying value, clean up, or raise to forbid deletion. Without one, del raises AttributeError.

@total.deleter def total(self): self._total = None # reset, forcing a recompute later

@property def derived(self): return f(self.a, self.b)

A property that computes its value from other attributes on every read, instead of storing its own — it can never go stale, at the cost of recomputing every time.

@property def full_name(self): return f"{self.first_name} {self.last_name}"

Object-Oriented Python — Dunder methods

12

def __new__(cls, *args, **kwargs): return super().__new__(cls)

Creates the object, before __init__ sets it up. Rarely overridden — mainly for singletons and subclassing an immutable built-in type.

class Config: _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance

oopdunderconstruction
__new__ and __init__

def __repr__(self): return f"Name({self.value!r})" def __str__(self): return f"{self.value}"

__repr__ is the unambiguous, developer-facing form (REPL, debugging, containers); __str__ is the reader-facing form used by print()/str(). Missing __str__ falls back to __repr__.

class Point: def __repr__(self): return f"Point({self.x!r}, {self.y!r})"

def __eq__(self, other): if not isinstance(other, Name): return NotImplemented return self.value == other.value

Defines what == compares — by value instead of Python's default identity check. __ne__ is derived automatically. Defining __eq__ disables hashing unless __hash__ is also defined.

p1 = Point(1, 2) p2 = Point(1, 2) p1 == p2 # True, via __eq__

def __lt__(self, other): return self.value < other.value

Defines < (and __le__ defines <=) — neither is derived from the other. @functools.total_ordering fills in all four ordering operators from __eq__ plus just one.

@total_ordering class Money: def __eq__(self, other): ... def __lt__(self, other): ...

def __gt__(self, other): return self.value > other.value

Defines > (and __ge__ defines >=). Neither is derived from __lt__/__le__. Python tries the reflected operator on the other operand before raising TypeError.

a, b = Money(700), Money(200) a > b # True, via __gt__

def __hash__(self): return hash((self.a, self.b))

Returns an int used to place an object in a dict/set. Must be consistent with __eq__: equal objects need equal hashes. Defining __eq__ alone disables hashing.

def __hash__(self): return hash((self.x, self.y)) # same fields as __eq__

oopdunder
__hash__

def __bool__(self): return self.some_condition

Defines truthiness — bool(obj) and if obj:. Without it, Python falls back to __len__ == 0 being falsy, then to always-truthy if neither is defined.

def __bool__(self): return len(self.items) > 0

oopdunder
__bool__

def __len__(self): return len(self._items) def __contains__(self, item): return item in self._items

__len__ defines len(obj); __contains__ defines "item in obj". Without __contains__, "in" falls back to a slower linear scan via __iter__.

len(cart) # via __len__ "pen" in cart # via __contains__

def __iter__(self): return self def __next__(self): if done: raise StopIteration return value

__iter__ makes an object iterable; __next__ makes it an iterator. A for loop is exactly iter() once, then next() until StopIteration.

for n in Countdown(3): print(n) # calls __iter__ once, __next__ repeatedly

oopdunderiteration
__iter__ and __next__

def __getitem__(self, key): return self._data[key] def __setitem__(self, key, value): self._data[key] = value

Defines obj[key] reads and writes. Independent hooks — a class can support only reads. Raise IndexError/KeyError for an invalid key, by convention.

s["theme"] = "dark" # via __setitem__ s["theme"] # via __getitem__

def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): return False

__enter__ runs at the top of a with block; __exit__ always runs at the end, even on an exception. Returning truthy from __exit__ suppresses it.

with Resource() as r: ... # __enter__ then, always, __exit__

oopdundercontext-manager
__enter__ and __exit__

def __call__(self, *args, **kwargs): return self.do_something(*args, **kwargs)

Lets an instance be called like a function — obj(args) is sugar for obj.__call__(args). Useful when state needs to persist across calls.

double = Multiplier(2) double(5) # 10, via __call__

oopdunder
__call__

Object-Oriented Python — Advanced OOP

6

class C(A, B): def method(self): super().method() # next in C.__mro__

Multiple inheritance gives a class several bases; the MRO (C3 linearization) is the single deterministic lookup order across them. super() follows that order, which is what makes cooperative inheritance work.

print(Diamond.__mro__) # (Diamond, Left, Right, Base, object)

class Widget(SomeMixin, Base): pass

A mixin is a small class adding one focused behaviour, meant to be combined via multiple inheritance rather than instantiated alone. List it before the real base class.

class LoggedWidget(LoggingMixin, Widget): pass

oopmixininheritance
Mixins

class Descriptor: def __get__(self, obj, owner): ... def __set__(self, obj, value): ...

A class implementing __get__/__set__, assigned as a CLASS attribute on another class, to intercept attribute access. @property is built from this protocol.

class Product: price = PositiveNumber() # reusable validation descriptor

oopdescriptoradvanced
Descriptors

def __getattr__(self, name): raise AttributeError(name) # fallback only def __setattr__(self, name, value): super().__setattr__(name, value)

__getattribute__ intercepts every read; __getattr__ only fires when normal lookup already failed; __setattr__ intercepts every write. Delegate to super() to avoid infinite recursion.

obj.x = 1 # __setattr__ runs obj.missing # __getattr__ runs, only if missing

class Name: __slots__ = ("a", "b")

Declares the only attribute names a class instance may hold, removing the per-instance __dict__ — lower memory, faster access, no arbitrary new attributes.

class Point: __slots__ = ("x", "y")

oopslotsperformance
__slots__

class Meta(type): def __new__(mcls, name, bases, namespace): return super().__new__(mcls, name, bases, namespace)

A metaclass is the class of a class — type builds every ordinary class by default. metaclass=Meta lets Meta control how a class itself is constructed.

class Name(metaclass=Meta): pass

oopmetaclassadvanced
Metaclasses

Python Internals — Object model

7

type(x) · isinstance(x, object)

Every Python value is an object — a number, string, function, module, or class. type(x) reports the class that built it; isinstance(x, object) is always True.

type(5) # <class 'int'>

object-modeltypeobjects
Everything is an object

x = obj · y = x · del x

A name references an object rather than containing it. Assignment binds a name to an existing object; del removes a name, and the object survives if any other name still references it.

y = x del x print(y) # still works — the object outlived the name x

object-modelreferencesnames
Names vs objects, and references

id(obj)

Returns an int unique among currently-alive objects, constant for the object's lifetime. In CPython, it is the object's memory address — but that is an implementation detail, and a freed id can be reused by a later object.

a is b # shorthand for id(a) == id(b)

object-modelidentityid
Object identity

def __eq__(self, other): ...

a == b calls a.__eq__(b). The default, inherited from object, falls back to identity — a class must override __eq__ before instances with equal fields compare equal.

def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y

object-modelequalitydunder
Equality

immutable: int, str, tuple, frozenset · mutable: list, dict, set

Mutability is decided by an object's type. An immutable object never changes after creation; a mutable one can. An immutable container (tuple) can still hold a mutable item that changes.

t = (1, [2, 3]) t[1].append(4) # legal — the list changes, the tuple does not

object-modelmutabilityimmutability
Mutability and immutability

hash(obj) · object.__hash__ (id-based default)

A plain class's inherited __hash__ is based on id(), not its attributes — two equal-looking instances hash differently. Defining __eq__ sets __hash__ to None until a matching one is supplied.

class Tag: def __eq__(self, other): ... def __hash__(self): return hash(self.name) # must agree with __eq__

__new__ -> __init__ -> (referenced) -> __del__

An object is created by __new__/__init__, stays alive while its reference count is above zero, and is destroyed the instant that count reaches zero — running __del__ first, if the class defines one.

obj = Tracked("x") # created del obj # refcount 0 -> __del__ runs

object-modellifecycledel
Object lifecycle

Python Internals — Memory management

8

sys.getrefcount(obj)

Reports how many references point at obj, including a temporary +1 for getrefcount's own argument. An object is freed the instant its true count reaches zero — CPython's primary memory-management mechanism.

import sys sys.getrefcount(obj) - 1 # the real count, before this call

memoryreference-countingsys
Reference counting

import gc · gc.collect() · gc.get_threshold()

The gc module runs a separate, periodic collector that finds and frees reference cycles — objects that only reference each other, which reference counting alone can never bring to zero.

gc.collect() # run now; returns the number of objects freed

a.other = b; b.other = a

A reference cycle: objects referencing each other in a loop. Reference counting alone cannot free one — either the gc module's cycle collector runs eventually, or a weakref in one direction avoids creating the cycle at all.

child._parent = weakref.ref(parent) # avoids the cycle up front

memorycyclesweakref
Cyclic references

sys.getsizeof(obj)

Reports the bytes obj occupies in CPython's private heap, including fixed per-object overhead (refcount, type pointer). Small objects (≤ 512 bytes) are served by pymalloc's arena allocator rather than a per-object malloc() call.

sys.getsizeof([]) # 56 — overhead alone, holding nothing

stack: frames + references · heap: every object

Python's call stack holds frames — one per active call — each containing references into the heap, where every object actually lives. Unlike C, there is no stack-allocated object data to reason about; a returned reference is always safe.

def f(): x = [1, 2, 3] # x: a frame reference; the list: on the heap return x # safe — the list never depended on the frame

weakref.ref(obj) · weakref.WeakValueDictionary()

Creates a reference that does not count toward obj's refcount. Call it to get the live object, or None once obj has been freed. WeakValueDictionary auto-removes entries whose value has been freed.

r = weakref.ref(obj) r() # obj, or None if freed

memoryweakrefreferences
Weak references

sys.intern(s)

Registers a string in CPython's shared intern pool, guaranteeing that future equal strings passed through sys.intern() share one object. Small ints (-5..256) and some string literals are interned automatically — never rely on that alone for correctness.

sys.intern("a") is sys.intern("a") # True, guaranteed

memoryinterningstrings
Interning

copy.copy(x) · copy.deepcopy(x)

copy.copy() builds a new outer container but shares nested objects with the original. copy.deepcopy() recursively copies every level, so nothing is shared. Plain assignment (y = x) copies nothing at all — it is a second name for the same object.

shallow["items"] is original["items"] # True — shared deep["items"] is original["items"] # False — independent

Python Internals — Execution model

7

f(args) -> new frame -> bind params -> run body -> return

Calling a function creates a new frame, binds each argument to its parameter name inside it, runs the body using that frame's local namespace, then discards the frame and returns the result.

def add(a, b): return a + b add(2, 3) # 5 — frame created, used, discarded

executionfunctionscalls
Function calls

sys._getframe() · frame.f_back · frame.f_locals

A frame holds one call's local variables, current line, and a link (f_back) to its caller's frame. The call stack is the chain of active frames, growing per call and shrinking per return — a traceback is that chain printed out.

frame = sys._getframe() frame.f_back.f_code.co_name # the caller's function name

executionstackframes
Call stack and frames

globals() · locals()

globals() is the real, live module-level namespace — safe to mutate. locals(), inside a function, is a snapshot dict rebuilt on each call — mutating it does not reliably affect real local variables.

globals()["x"] = 1 # a real edit locals()["x"] = 1 # inside a function: usually has no effect

executionnamespacesscope
Local and global namespaces

func.__closure__ · cell.cell_contents

func.__closure__ is a tuple of cell objects, one per captured variable, matching func.__code__.co_freevars. Functions from the same enclosing call share a cell; separate calls each get their own.

add5.__closure__[0].cell_contents # 5 — the captured value of n

executionclosurescells
Closures (the cell mechanism)

func.__code__.co_freevars

Lists the names a function reads (never assigns) that come from an enclosing function's scope — decided at compile time by scanning the whole function body. An assignment anywhere in the body makes a name local instead.

inner.__code__.co_freevars # ('value',) — value is read but never assigned in inner

executionclosuresscope
Free variables

lambda x=x: ... # freezes late-bound x at definition time

A function body's names are looked up when the function runs, not when it is defined — every call re-resolves them fresh. This is why closures over a loop variable all see its final value, unless frozen via a default argument.

handlers.append(lambda event_type=event_type: ...) # captures THIS iteration's value

executionclosuresscope
Late binding

import dis · dis.dis(func)

Python source compiles to bytecode before running, executed by CPython's stack-based virtual machine. dis.dis() shows a function's bytecode. Not guaranteed stable across Python versions or implementations — CPython is the reference implementation, not the only one.

dis.dis(add) # prints the compiled bytecode instructions for add

Functional Programming

8

def f(x): return g(x) # no globals, no mutation, no I/O

A pure function returns a value that depends only on its arguments and has no side effects — no global write, no argument mutation, no I/O. Same input always gives the same output.

def add_tax(price, rate): return price * (1 + rate)

functionalpurityside-effects
Pure functions

compose(f, g) = lambda x: f(g(x))

Chains functions so each one's output feeds the next one's input. Python has no built-in compose() — write a small helper, or fold a list of functions with reduce().

shout = compose(str.upper, str.strip) shout(" hi ") # 'HI'

functionalcompositionpipelines
Function composition

functools.reduce(func, iterable, initial=...)

Folds an iterable into one value by calling func(accumulator, item) left to right. Without initial, the first item seeds the accumulator; an empty iterable with no initial raises TypeError. Always eager, never lazy.

reduce(lambda acc, n: acc + n, [1, 2, 3, 4], 0) # 10

functoolsfunctionaliteration
reduce

import functools

The standard-library module for function-level tools: reduce (folding), partial (argument binding), lru_cache/cache (memoization), wraps (preserving metadata through a decorator), and total_ordering.

from functools import reduce, partial, lru_cache

functoolsstdlibfunctional
functools

functools.partial(func, *args, **kwargs)

Returns a new callable with some of func's arguments already bound. Positional arguments fill the earliest unbound slots; keyword arguments bind by name and can still be overridden at call time unless func forbids it.

square = partial(power, exponent=2) square(5) # 25

functoolsfunctionalpartial-application
functools.partial

map(...) · filter(...) · (x for x in ...) · a generator function

A lazy expression is built instantly and computes nothing until iterated. A list comprehension is the opposite — eager, computing every item immediately.

gen = (n * n for n in big_source) # instant; next(gen) computes one item

functionallazygeneratorsiterators
Lazy evaluation

stage_two(stage_one(source))

Chains generator functions so each stage yields to the next, one item at a time. No intermediate list is ever built — memory stays flat regardless of source size, as long as every stage stays lazy.

pipeline = positive_only(parse(nonempty(strip_lines(read_lines()))))

generatorslazypipelinesfunctional
Generator pipelines

tuple · frozenset · namedtuple · @dataclass(frozen=True)

Data structures that refuse mutation after creation, so a value can be shared without defensive copying. Nesting a mutable type (a list) inside one of these does not make that inner object immutable too.

@dataclasses.dataclass(frozen=True) class Config: host: str

immutabilityfunctionaldata-design
Immutability concepts

Decorators

8

@my_decorator def process(): ...

Sugar for process = my_decorator(process), run once at def time. my_decorator returns a wrapper that stands in for the original function.

def my_decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper

decoratorsfunctionsclosures
Function decorators

def outer(config): def decorator(func): def wrapper(*args, **kwargs): ... return wrapper return decorator

A decorator that takes its own arguments needs three nested functions — the outer one for the decorator's config, then decorator(func), then wrapper(...).

@repeat(times=3) def greet(name): return f"Hi, {name}"

def factory(config): def decorator(func): ... return wrapper return decorator

A plain function that returns a decorator. Calling it with different arguments produces independent decorators, each closing over its own configuration.

@validate_range(0, 100) def set_volume(level): return f"volume set to {level}"

decoratorsclosures
Decorator factories

class Name: @my_decorator def method(self, ...): ...

A decorator applies to a method exactly like a plain function — self simply arrives as the first positional argument inside wrapper.

@log_call def deposit(self, amount): self.balance += amount

decoratorsoopmethods
Method decorators

@my_decorator class Name: ...

Sugar for Name = my_decorator(Name) — the decorator receives the class object itself and must return a class, usually the same one modified.

def add_repr(cls): cls.__repr__ = ... return cls

decoratorsoopclasses
Class decorators

@bold @italic def f(): ... # == f = bold(italic(f))

Multiple decorators apply bottom-up at decoration time (closest to def first) but run outer-to-inner at call time (topmost decorator's wrapper runs first).

@bold @italic def shout(text): return text.upper()

decoratorsorder
Stacked decorators

@functools.wraps(func) def wrapper(*args, **kwargs): ...

Copies __name__, __doc__ and __module__ from func onto wrapper, and sets wrapper.__wrapped__ = func. Without it, a decorated function reports wrapper's identity instead of its own.

import functools @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs)

decoratorsfunctoolsintrospection
functools.wraps

class Name: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): return self.func(*args, **kwargs)

A class implementing __call__ can act as a decorator — @Name above a def runs Name(func), and the resulting instance replaces the function, with state stored as plain attributes on self.

@CountCalls def say_hello(name): return f"Hello, {name}"

decoratorsoopcall-dunder
Callable objects

Iterators and Generators

9

iterable: __iter__() -> iterator · iterator: __next__() -> value

An iterable produces iterators; an iterator produces values. iter(iterable) gets a fresh iterator; next(iterator) advances it.

it = iter([1, 2, 3]); hasattr(it, "__next__") # True

iterationiteratorsprotocol
Iterable vs iterator

__iter__(self) -> iterator · __next__(self) -> value | raise StopIteration

The two-method contract behind for, list(), sum(), and unpacking. A generator function satisfies it automatically.

list(count_up_gen(3)) # works exactly like list(CountUpIterator(3))

iterationprotocolgenerators
Iterator protocol

def name(): yield value

Any function containing yield becomes a generator function. Calling it builds a paused generator; next() runs it to the next yield.

gen = read_batches(7, 3); next(gen) # [0, 1, 2]

yield from iterable

Delegates to iterable, yielding each of its items and forwarding send()/throw()/close() to it.

def flatten(x): for i in x: yield from flatten(i) if isinstance(i, list) else [i]

generatorsyielddelegation
yield from

[expr for x in data] (eager) · (expr for x in data) (lazy)

Square brackets compute every item immediately; round brackets compute each item only when asked.

sum(transform(x) for x in data) # transform() runs one x at a time, inside sum()

generatorslazycomprehensionsmemory
Lazy evaluation

stage_n(...stage_2(stage_1(source)))

Nested generator calls compose into a pipeline: each item flows through every stage before the next item enters.

list(filter_even(parse_ints(read_lines(raw_lines))))

generatorspipelineslazycomposition
Generator pipelines

sys.getsizeof(a_generator) # constant, regardless of item count

A generator (or a pipeline of them) holds one item at a time — flat memory. A list holds every item at once — memory scales with count.

sum(1 for _ in (f(x) for x in huge_source)) # never builds a list

generatorsmemoryperformancelazy
Memory-efficient processing

gen.send(value)

Resumes a paused generator, delivering value as the result of its current yield expression. Requires priming with next() first.

next(gen); gen.send(10) # gen must already be paused on a yield

generatorsyieldsendcoroutines
Sending values into generators

gen.close()

Raises GeneratorExit at the generator's current pause point, running any finally block, then leaves it exhausted.

gen = managed_resource(); next(gen); gen.close() # finally block runs

generatorscleanupexceptions
Generator cleanup

Exception Handling — Fundamentals

9

BaseException > Exception > ValueError, KeyError, OSError, ...

Every exception inherits BaseException; Exception is the subclass for ordinary, handleable errors. SystemExit/KeyboardInterrupt/GeneratorExit sit outside Exception.

issubclass(KeyboardInterrupt, Exception) # False

exceptionshierarchy
Exception hierarchy

try: ... except SomeError as e: ...

try wraps code that might raise; except names the exception type to catch. Name a specific type rather than a bare except:.

try: result = 10 / n except ZeroDivisionError: result = None

exceptionstryexcept
try / except

try: ... except E: ... else: ... # only if try raised nothing

else runs only when the try block completed without raising — keeps success-path code from being caught by except.

try: value = int(text) except ValueError: value = None else: print(value * 2)

exceptionstryelse
else clause

try: ... finally: cleanup()

finally always runs, regardless of whether the try block succeeded, raised, or returned — the place for guaranteed cleanup.

f = open(path) try: process(f) finally: f.close()

exceptionstryfinallycleanup
finally clause

except (TypeA, TypeB) as e: ...

Catches either exception type with one shared handler. Requires parentheses — a bare comma is Python 2 syntax and raises SyntaxError in Python 3.

except (TypeError, ValueError) as e: return f"bad input: {e}"

exceptionstryexcept
Multiple exception types

raise SomeError("message")

Constructs and raises an exception in one step; a bare raise inside except re-raises the current exception unchanged.

if amount < 0: raise ValueError(f"amount cannot be negative: {amount}")

exceptionsraise
raise

raise New(msg) from original

Explicitly sets New.__cause__ = original; the traceback shows both, labelled "the direct cause of the following exception."

raise ConfigError("invalid config value") from e

exceptionsraisechaining
raise ... from ...

except A: raise B(...) # __context__ set automatically, no `from` needed

Any raise inside an active except block sets __context__, even with no explicit `from` — the traceback reads "During handling of the above exception."

raise NewError("cleanup also failed") from None # suppress the implicit chain

exceptionschainingtraceback
Exception chaining

class MyError(Exception): def __init__(self, ...): super().__init__(message)

Subclass Exception to define a project-specific error type; override __init__ to attach structured data and still call super().__init__(message).

raise InsufficientFundsError(balance=100, amount=150)

exceptionscustomoop
Custom exceptions

Exception Handling — Production error handling

10

def boundary(...): try: return inner_call(...) except SpecificError as e: raise StableError(...) from e

The one place in a call chain — an entry point — that catches internal exceptions broadly and translates them into a stable, intentional response.

raise ExternalAPIError(f"upstream call failed: {e}") from e

exceptionsproductionarchitecture
Exception boundaries

# no try/except needed — this is the default behaviour

An exception with no matching except in the current frame automatically continues up the call stack to the nearest caller that has one.

def parse_row(row): return int(row) # lets ValueError propagate on purpose

exceptionsproduction
Error propagation

RETRYABLE = (ConnectionError, TimeoutError, RateLimitError)

Errors caused by a transient condition, where retrying the identical call has a real chance of succeeding. Defined once as a fixed set, reused everywhere.

if isinstance(exc, RETRYABLE): retry()

exceptionsproductionretry
Retryable errors

NON_RETRYABLE = (ValueError, InvalidRequestError)

Errors caused by the request itself, not a transient condition — retrying the identical call cannot succeed. Fail fast rather than looping.

if isinstance(exc, NON_RETRYABLE): raise # surface immediately, do not retry

exceptionsproductionretry
Non-retryable errors

requests.get(url, timeout=5)

Bounds how long an operation may run before raising a timeout error — without one, a hung call can block indefinitely.

try: requests.get(url, timeout=5) except requests.Timeout: handle_timeout()

exceptionsproductiontimeout
Timeouts

for attempt in range(1, attempts + 1): try: return fn() except RETRYABLE: ...

A retry loop needs a hard cap on attempts, a delay between them, and a re-raise of the final failure once attempts are exhausted.

retry_fixed(call_api, attempts=3, delay=1)

exceptionsproductionretry
Retry strategies

delay = min(base * (2 ** (attempt - 1)), cap)

Doubles the retry delay each attempt, capped at a maximum. Add random jitter (e.g. random.uniform(0, delay)) to avoid synchronized retries.

[compute_backoff(1.0, n, 30.0) for n in range(1, 8)] # [1.0, 2.0, 4.0, 8.0, 16.0, 30.0, 30.0]

exceptionsproductionretrybackoff
Exponential backoff

logger.exception(message)

Called inside an except block; logs at ERROR level with the full traceback attached automatically. Equivalent to logger.error(message, exc_info=True).

except ValueError: logger.exception("risky_operation failed")

exceptionsproductionlogging
Logging exceptions

logger.error(detail); return {"error": generic_message}

Log full exception detail internally; return a short, generic message externally — internal detail exposed to a caller is an information leak.

except OrderError as e: logger.error("order failed: %s", e) return {"error": "We could not process your order."}

exceptionsproductionsecurity
User-facing vs internal errors

except Exception: pass # AVOID unless intentionally justified with a comment

Discarding an exception with no log, metric, or re-raise hides real failures. Log at minimum, and catch the narrowest type actually expected.

except ValueError as e: logger.error("skipping bad row: %s", e) continue

exceptionsproductionanti-pattern
Avoiding swallowed exceptions

Context Managers

6

with expression as name: ...

Runs expression.__enter__() before the block and expression.__exit__() after — even if the block raises.

with open("data.txt") as f: contents = f.read()

context-managerwithresource-management
The with statement

from contextlib import contextmanager, suppress, closing, ExitStack

Standard library module for building and combining context managers without writing __enter__/__exit__ by hand.

with suppress(FileNotFoundError): open("missing.txt")

context-managercontextlibstdlib
The contextlib module

@contextmanager def name(arg): # setup try: yield value finally: # teardown

Turns a generator function into a context manager — code before yield is setup, code after yield (in finally) is teardown.

with my_manager("x") as value: ...

context-managercontextlibgenerator
@contextlib.contextmanager

with a() as x, b() as y: ...

Enters a then b, left to right; exits b then a — last entered, first exited.

with open("in.txt") as fin, open("out.txt", "w") as fout: fout.write(fin.read())

context-managerwithnesting
Nested context managers

async def __aenter__(self): ... async def __aexit__(self, exc_type, exc_value, traceback): ...

The async counterpart of __enter__/__exit__ — both coroutines, awaited by async with, for setup/teardown that itself awaits something.

async with AsyncConnection() as conn: await conn.query(...)

with acquire_resource() as thing: use(thing)

The reason to reach for a context manager: guaranteed cleanup for files, transactions, locks, connections, and temporary state.

with lock: process_shared_data() # released no matter what

context-managermotivationcleanup
Why context managers matter

Type Hints and Static Typing — Core typing

7

x: int = 5 def f(a: str, b: int = 1) -> str: ...

Type annotation syntax for a variable, a function parameter, and a return value. Documentation only — not enforced at runtime.

def greet(name: str) -> str: return "Hi " + name

typingannotations
Basic annotations

list[T] · dict[K, V] · tuple[T, ...] · set[T]

Built-in container generics (PEP 585). Work directly since Python 3.9 — no typing.List/Dict/Tuple/Set import needed.

scores: dict[str, int] = {"Ana": 90}

typinggenericscollections
Generic collection annotations

Optional[X] == X | None Union[X, Y] == X | Y

Optional[X] means X or None. Union[X, Y] means X or Y. The X | None / X | Y spelling (PEP 604, Python 3.10+) is the modern equivalent.

def get_user(user_id: int) -> User | None: ...

typingoptionalunion
Optional and Union types

Any · Literal["a", "b"] · Final = value

Any disables checking for a value. Literal restricts to specific values. Final marks a name as not meant to be reassigned. None enforced at runtime.

MAX_RETRIES: Final = 3 Mode = Literal["r", "w"]

typinganyliteralfinal
Any, Literal, and Final

Callable[[ArgType, ...], ReturnType]

Annotates a value that is itself a function with a given signature. Use Callable[..., T] for "any arguments."

def apply(fn: Callable[[int, int], int]) -> int: return fn(1, 2)

typingcallable
Callable

T = TypeVar("T") class Box(Generic[T]): ...

TypeVar declares a placeholder type name; Generic[T] makes a class parametrizable over it.

b: Box[int] = Box(5) b.get() # type checker knows this is int

typingtypevargeneric
TypeVar and Generic

Name: TypeAlias = ExistingType Annotated[Type, metadata, ...]

TypeAlias names an existing type for reuse in signatures. Annotated attaches extra metadata to a type without changing the type itself.

UserId: TypeAlias = int PositiveInt = Annotated[int, "must be > 0"]

typingtypealiasannotated
TypeAlias and Annotated

Type Hints and Static Typing — Advanced typing

4

class Name(TypedDict): key: Type

Declares a dict with fixed, known keys and a type per key. Produces a plain dict at runtime — checked only by a static type checker.

class Movie(TypedDict): title: str year: int

typingtypeddict
TypedDict

if isinstance(x, T): ... # auto-narrows def guard(x) -> TypeGuard[T]: ... # custom

Type narrowing shrinks a Union to one branch after a check like isinstance(). A type guard is a custom function that teaches a type checker a narrowing rule.

if isinstance(value, str): value.upper() # narrowed to str

typingnarrowingtypeguard
Type narrowing and type guards

def f(x: list[T]) -> T: ... class C(Generic[T]): ...

A generic function reuses a TypeVar between parameter and return type. A generic class inherits Generic[T] and uses T across its methods.

def first(items: list[T]) -> T: return items[0]

TypeVar("T", covariant=True | contravariant=True)

Variance: whether a generic container built from a subtype may substitute for one built from its supertype. Covariant for read-only, contravariant for write-only, invariant (default) for both.

T_co = TypeVar("T_co", covariant=True) class ReadOnlyBox(Generic[T_co]): ...

typingvariancegenerics
Variance (conceptual)

Type Hints and Static Typing — Tools

2

mypy file.py npx pyright file.py

The two mainstream Python static type checkers. Both analyze annotations without running the code; mypy is the Python-native original, pyright (Pylance) is Microsoft's, generally faster.

pip install mypy mypy app.py

typingmypypyrighttools
mypy vs. pyright

mypy --strict . # CI: pip install mypy && mypy .

IDE integration checks live as you type; --strict enables every optional check; running the checker in CI is what makes it mandatory for every change.

- run: pip install mypy && mypy .

Dataclasses and Data Modeling

8

@dataclass class Name: field1: type field2: type = default

Generates __init__, __repr__, and __eq__ from a class's annotated fields.

@dataclass class Order: order_id: str total_cents: int

dataclassoopboilerplate
@dataclass

@dataclass(frozen=True) class Name: ...

Blocks attribute assignment after construction — raises FrozenInstanceError on obj.field = value.

@dataclass(frozen=True) class Coordinates: lat: float lon: float

dataclassimmutabilityfrozen
Frozen dataclasses

field(default_factory=list, init=True, repr=True, compare=True, metadata=None)

Configures one field beyond a plain default — required for any mutable default value.

items: list = field(default_factory=list)

def __post_init__(self): ...

Runs automatically as the last step of the generated __init__, after every field is assigned.

def __post_init__(self): if self.value < 0: raise ValueError("must be non-negative")

dataclassvalidationpost_init
Post-init processing

@dataclass(order=True) class Name: ...

Generates __lt__/__le__/__gt__/__ge__, comparing fields as a tuple in declaration order.

@dataclass(order=True) class Version: major: int minor: int

dataclassorderingcomparison
Ordering

@dataclass class Sub(Base): ...

Inherits the parent dataclass's fields, combined into one __init__ — parent fields first, in order.

@dataclass class Dog(Animal): breed: str = "mixed"

dataclassinheritance
Dataclass inheritance

@dataclass(slots=True) class Name: ...

Generates __slots__ from the field list (Python 3.10+) — no __dict__, lower memory, no undeclared attributes.

@dataclass(slots=True) class Point3D: x: float y: float z: float

dataclassslotsperformance
slots=True

@dataclass(frozen=True, slots=True) class Name: ... # value object

A regular class needs hand-written boilerplate; @dataclass generates it but never validates; Pydantic validates and coerces at construction. A value object is usually a frozen dataclass.

@dataclass(frozen=True) class Money: amount_cents: int currency: str

Standard Library

18

Counter(iterable) · defaultdict(factory) · namedtuple(name, fields) · deque(iterable, maxlen=None)

Four specialized containers for counting, auto-initializing, naming fields, and fast double-ended queues.

Counter(words).most_common(3)

stdlibcollectionscontainers
collections

chain(*its) · islice(it, stop) · groupby(it, key) · product/combinations(it, r)

Lazy building blocks for chaining, slicing, grouping, and generating combinations over iterators.

list(chain([1, 2], [3, 4]))

stdlibitertoolsiterators
itertools

@lru_cache(maxsize=128) · reduce(fn, it, start) · partial(fn, *args) · @wraps(fn)

Caching, folding, argument pre-filling, and metadata preservation for functions.

@lru_cache(maxsize=None) def fib(n): ...

stdlibfunctoolsdecorators
functools

isinstance(x, collections.abc.Sequence)

Abstract base classes checking capability (Sequence, Mapping, Iterable) rather than one concrete type.

isinstance([1, 2, 3], collections.abc.Sequence)

stdlibcollections.abcabctyping
collections.abc, abc, and typing

Path(*parts) / "next" · .name/.stem/.suffix/.parent · .exists()/.read_text()/.glob(pattern)

Object-oriented filesystem paths — joining, reading pieces, and checking the filesystem.

Path("data") / "file.csv"

stdlibpathlibfilesystem
pathlib

datetime(y, m, d, h, mi, s, tzinfo=ZoneInfo("Region/City")) · timedelta(days=, hours=)

Dates, times, durations, and real IANA time zones with correct DST handling.

datetime.now(ZoneInfo("Europe/London"))

stdlibdatetimezoneinfotimezones
datetime and zoneinfo

os.environ.get(k, default) · sys.argv · subprocess.run([...], capture_output=True, text=True)

Environment variables and OS paths (os), interpreter state (sys), and running other programs (subprocess).

subprocess.run(["git", "status"], capture_output=True, text=True)

@contextmanager def name(): ... yield ...

Build a context manager from a generator function instead of a class — full depth in Context Managers.

@contextmanager def timer(): yield

stdlibcontextlibcontext-managers
contextlib

async def f(): ... · await expr · asyncio.run(main()) · asyncio.gather(*coros)

Single-threaded concurrency for I/O-bound work — full depth in the Async Python section.

asyncio.run(asyncio.gather(fetch(a), fetch(b)))

stdlibasyncioconcurrency
asyncio

with ThreadPoolExecutor(max_workers=N) as pool: pool.map(fn, items) / pool.submit(fn, *args)

A uniform pool API over threads (I/O-bound) or processes (CPU-bound).

list(pool.map(square, [1, 2, 3, 4]))

stdlibconcurrencythreadsprocesses
concurrent.futures

logging.getLogger(__name__).warning(msg, *args)

Leveled, filterable, routable output — DEBUG through CRITICAL, via loggers, handlers, and formatters.

logger.error("failed to save order %s: %s", order_id, exc)

stdlibloggingdebugging
logging

re.search(pattern, s) · re.findall(pattern, s) · re.sub(pattern, repl, s)

Pattern matching, extraction, and replacement against strings.

re.sub(r"\s+", " ", text)

json.dumps(obj, indent=2, sort_keys=True) · json.loads(s)

Serialize Python objects to JSON text and parse JSON text back.

json.dumps({"active": True}, indent=2)

stdlibjsonserialization
json

class X(Enum): NAME = value · X.NAME.value · list(X) · X(value)

A fixed, named, comparable set of values as a real type.

class Status(Enum): PENDING = "pending"

stdlibenum
enum

parser.add_argument("--flag", type=int, default=3, action="store_true")

Declarative command-line argument parsing with automatic --help.

parser.parse_args(["file.csv", "--verbose"])

stdlibargparsecli
argparse

@dataclass class Name: field: type

Auto-generates __init__/__repr__/__eq__ from annotated fields — full depth in Dataclasses and Data Modeling.

@dataclass class Point: x: int y: int

stdlibdataclasses
dataclasses

Decimal("19.99") · Fraction(1, 3) · statistics.mean(data)

Exact decimal arithmetic, exact rational arithmetic, and summary statistics — three numeric modules beyond plain float.

Decimal("0.1") + Decimal("0.2") == Decimal("0.3")

uuid.uuid4() · secrets.token_hex(n) · hashlib.sha256(data).hexdigest() · shutil.copy(src, dst)

Six single-purpose stdlib modules: embedded database, unique ids, secure randomness, scratch files, file operations, hashing.

hashlib.sha256(b"data").hexdigest()

Concurrency and Parallelism — Concepts

4

concurrency: interleaved · parallelism: simultaneous

Concurrency structures work as multiple interleavable tasks; parallelism actually runs tasks at the same instant, which requires multiple cores.

threading.Thread(...) # concurrency multiprocessing.Process(...) # parallelism

concurrencyparallelism
Concurrency vs parallelism

CPU-bound -> multiprocessing · I/O-bound -> threading/asyncio

CPU-bound work is limited by computation and needs separate cores to speed up; I/O-bound work is limited by waiting and speeds up by overlapping those waits.

with ProcessPoolExecutor() as ex: ex.map(cpu_heavy, items) with ThreadPoolExecutor() as ex: ex.map(fetch_url, urls)

concurrencyperformancegil
CPU-bound vs I/O-bound workloads

Thread/Process (one-off) · ThreadPoolExecutor/ProcessPoolExecutor (reused pool)

Four building blocks: a single thread or process for a one-off task, or a thread/process pool for many tasks of the same shape.

with ThreadPoolExecutor(max_workers=8) as ex: ex.map(fetch_url, urls)

async def f(): ... · await f() · asyncio.run(main())

A coroutine pauses only at an explicit await, letting the event loop run another coroutine on the same thread — a fit for many concurrent I/O waits.

results = await asyncio.gather(fetch("a"), fetch("b"))

concurrencyasyncioasync
Asynchronous programming

Concurrency and Parallelism — Threading

5

threading.Thread(target=fn, args=(...)).start() / .join()

Builds and runs a thread with .start(), and blocks the caller until it finishes with .join().

t = threading.Thread(target=download, args=("file.csv",)) t.start() t.join()

threadingconcurrency
threading and Thread

with threading.Lock(): ... · with threading.RLock(): ...

Lock allows only one thread in at a time, including re-entry by its own holder (which deadlocks). RLock allows the owning thread to re-acquire.

lock = threading.Lock() with lock: counter += 1

threadinglockconcurrency
Lock and RLock

Event().wait()/.set() · Condition().wait_for(pred) · Semaphore(n)

Event signals a one-off happening, Condition waits for shared state to become true, Semaphore(n) limits concurrent access to n holders.

sem = threading.Semaphore(2) with sem: do_limited_work()

threadingconcurrencysynchronization
Event, Condition, and Semaphore

queue.Queue() — thread-safe put()/get(), no external lock needed

A thread-safe structure handles its own internal locking; queue.Queue is the standard way to hand off work between threads safely.

q = queue.Queue() q.put(item) # from any thread item = q.get() # from any thread, blocks until available

race: lost update · deadlock: circular lock wait · starvation: perpetual denial

Three concurrency failure modes: unsynchronized access loses updates, circular lock waiting deadlocks, and unfair scheduling starves a thread.

first, second = sorted([lock_a, lock_b], key=id) with first: with second: ...

threadingconcurrencydeadlockrace-condition
Race conditions, deadlocks, and starvation

Concurrency and Parallelism — Multiprocessing

2

mp.Process(target=fn, args=(...)) · with mp.Pool(processes=n) as pool: pool.map(fn, items)

Runs work in real, separate OS processes — genuine CPU parallelism, but every argument and result crosses the process boundary by pickling.

with mp.Pool(processes=2) as pool: results = pool.map(cpu_heavy, [n1, n2])

multiprocessingprocessparallelism
multiprocessing and Process

mp.Queue() / mp.Pipe() · shared_memory.SharedMemory(create=True, size=n)

Queue/Pipe move data between processes by pickling it; shared_memory gives multiple processes a raw buffer with no pickling, at the cost of manual close()/unlink() bookkeeping.

shm = shared_memory.SharedMemory(create=True, size=10) shm.buf[0] = 42

multiprocessingipcpickleshared-memory
IPC, serialization, and shared memory

Concurrency and Parallelism — Futures

2

with ThreadPoolExecutor(max_workers=n) as ex: ex.submit(fn, *args)

A unified pool interface — ThreadPoolExecutor for I/O-bound work, ProcessPoolExecutor for CPU-bound work — where submit()/map() return Futures instead of blocking immediately.

with ThreadPoolExecutor(max_workers=2) as ex: f = ex.submit(fetch_url, url) result = f.result()

concurrent.futuresthreadpoolexecutorprocesspoolexecutor
ThreadPoolExecutor and ProcessPoolExecutor

future.result(timeout=None) · as_completed(futures) · future.cancel()

.result() blocks and re-raises the task's exception; as_completed() yields futures as they finish; cancel() only succeeds before the task starts running.

for future in as_completed(futures): print(future.result())

concurrent.futuresfuturecancellation
Futures, result handling, and cancellation

Concurrency and Parallelism — GIL

2

sys._is_gil_enabled()

The GIL is CPython's single mutex around Python bytecode execution, existing to keep reference counting safe without per-object locks.

import sys print(sys._is_gil_enabled()) # True on a standard build

CPU-bound → ProcessPoolExecutor · I/O-bound → ThreadPoolExecutor / asyncio

The GIL makes CPU-bound threading ineffective (sometimes slower than serial) but leaves I/O-bound threading genuinely effective, since blocking I/O releases the GIL.

Measured: CPU-bound 2 threads 0.757s vs. 2 processes 0.470s vs. serial 0.641s. I/O-bound 2 threads 0.302s vs. serial 0.601s.

Async Python

12

async def f(): ... · await f() · asyncio.run(main())

async def builds a coroutine function; calling it returns a paused coroutine object; await is what actually runs it.

result = await greet("Ada")

asyncioasyncevent-loop
Event loop and coroutines

asyncio.create_task(coro())

Schedules a coroutine to start running immediately, in the background, and returns an awaitable Task.

t = asyncio.create_task(worker(4)) result = await t

await asyncio.gather(*coros, return_exceptions=False)

Runs coroutines concurrently, returns results in call order once all finish.

results = await asyncio.gather(fetch("a"), fetch("b"))

task.cancel() · async with asyncio.timeout(seconds): ...

Both interrupt a coroutine at its next await point — cancel() on demand, timeout() automatically after a deadline.

async with asyncio.timeout(2): await slow_operation()

asyncioasynccancellationtimeout
Cancellation and timeouts

async def __aenter__(self) · async def __aexit__(self, exc_type, exc, tb)

The async equivalents of __enter__/__exit__, used with async with for resources whose open/close step is itself I/O.

async with AsyncDBConnection() as conn: await conn.query("SELECT 1")

asyncioasynccontext-manager
Async context managers

async def gen(): ... yield x ... · async for x in gen(): ...

An async def function with yield is an async generator — Python builds __aiter__/__anext__ automatically.

async for v in countdown_gen(3): print(v)

asyncioasyncgeneratoriterator
Async iterators and async generators

sem = asyncio.Semaphore(n) · async with sem: ...

Caps how many coroutines can be inside the block at once; Lock() is the same idea with a cap of exactly 1.

async with asyncio.Semaphore(2): await fetch_page(n)

asyncioasyncsemaphorelock
Semaphores and async locks

await asyncio.to_thread(blocking_fn, *args)

Runs a blocking function in a worker thread and awaits its result, without freezing the event loop.

result = await asyncio.to_thread(blocking_io)

asyncioasyncblockingproduction
Blocking code inside async applications

async with httpx.AsyncClient() as client: await client.get(url)

An async HTTP client awaits network I/O instead of blocking the event loop; reuse one instance as a connection pool.

async with httpx.AsyncClient() as client: r = await client.get(url)

asyncioasynchttpdatabaseconnection-pool
Async HTTP/database clients and connection pooling

asyncio.Queue(maxsize=n)

A bounded queue whose put() blocks once full — forces a fast producer to wait for a slower consumer, applying backpressure automatically.

queue = asyncio.Queue(maxsize=100) await queue.put(item) # blocks if full

asyncioasyncbackpressureproduction
Backpressure and concurrency limits

try: await work() finally: await cleanup()

try/finally guarantees cleanup runs on cancellation exactly like any other exception — the standard shape for graceful cancellation.

try: await do_work() finally: await resource.release()

asyncioasynccancellationproduction
Graceful cancellation

Threading → I/O-bound sync · Multiprocessing → CPU-bound · AsyncIO → high-concurrency I/O-bound

The three concurrency tools, matched to the three workload shapes the roadmap calls out.

CPU-bound -> ProcessPoolExecutor I/O-bound (few) -> ThreadPoolExecutor I/O-bound (many) -> asyncio.gather

asynciothreadingmultiprocessingconcurrencyproduction
Choosing threading, multiprocessing, or asyncio

Testing — Test types

2

Unit → Integration → API → End-to-end

The test pyramid: fast and narrow at the base, slow and broad at the top — most coverage should be in the base.

def test_unit(): ... def test_integration(db): ... def test_api(client): ...

test_regression_<bug-id>_<what-it-guards>()

A regression test names the bug it guards against; a contract test checks response shape; a performance test asserts a time/resource budget.

def test_regression_1234_negative_quantity_returns_zero(): assert calculate_total(-1, 10) == 0

Testing — pytest

5

test_*.py · def test_*(): assert ...

pytest discovers tests by naming convention and rewrites plain assert statements to show exactly what failed.

def test_add_returns_sum(): assert add(2, 3) == 5

testingpytestassertions
Test discovery and assertions

@pytest.fixture(scope="function") def name(): yield value

Marks reusable setup/teardown, injected into any test naming it as a parameter — scope controls how often it rebuilds.

@pytest.fixture def sample_order(): return {"id": 1} def test_x(sample_order): ...

@pytest.mark.parametrize("a,b", [(1,2), (3,4)])

Runs the decorated test once per tuple of values, each reported as its own separate test ID.

@pytest.mark.parametrize("a,b,expected", [(2,3,5)]) def test_add(a, b, expected): assert a + b == expected

testingpytestparametrize
Parametrization

@pytest.mark.skip/skipif/xfail · pytest -m "expr"

Marks tag tests for selection or special handling; -m filters the run by mark expression.

@pytest.mark.skipif(sys.platform == "win32", reason="posix-only") def test_x(): ...

testingpytestmarksplugins
Marks and plugins

conftest.py: @pytest.fixture def name(): ...

Fixtures in conftest.py are auto-discovered by every test file in the same directory tree, without an import.

# conftest.py @pytest.fixture def api_client(): return build_client()

Testing — Mocking

5

Mock(return_value=X) · mock.assert_called_once_with(...)

A stand-in object that records calls and returns a configured value; MagicMock adds dunder method support.

mock_send = Mock(return_value=True) mock_send("a@b.com") mock_send.assert_called_once_with("a@b.com")

testingmockingunittest.mock
Mock and MagicMock

with patch("module.name") as mocked: ... · AsyncMock(return_value=X)

patch swaps a name for a Mock for a scoped duration; AsyncMock is the awaitable Mock variant for coroutines.

fetcher.fetch = AsyncMock(return_value={"ok": True}) await fetcher.fetch(url)

testingmockingpatchasyncmock
patch and AsyncMock

monkeypatch.setenv/setattr/setitem(...)

A built-in fixture for temporary changes that automatically reverse after the test, pass or fail.

def test_x(monkeypatch): monkeypatch.setenv("API_KEY", "test-key")

testingpytestmonkeypatch
Monkeypatching

Stub (canned answer) · Spy (records + real work) · Fake (simplified real impl)

The vocabulary for test doubles by what they actually do, not just "mock" for everything.

class FakeRepository: def save(self, x): self._data[x["id"]] = x

Mock the boundary, not the thing under test

External dependencies (network, DB, payment, clock) are mock candidates; the function/class the test exists to check is not.

def test_places_order(mock_payment_gateway): place_order(cart, mock_payment_gateway)

Testing — Quality

4

pytest --cov=module --cov-report=term-missing

Reports the percentage of lines executed by the test suite, and exactly which lines were missed.

pytest --cov=calc --cov-report=term-missing

testingpytestcoverage
Coverage

No shared mutable state · no dependence on real time/randomness/order

Isolated tests do not depend on other tests' side effects; deterministic tests give the same result every run.

def test_is_expired(): order = Order(created_at=datetime(2026, 1, 1)) assert order.is_expired(now=datetime(2026, 2, 1))

testingpytesttest-isolation
Test isolation and determinism

@pytest.fixture: begin transaction -> yield -> rollback

Wraps a test in a transaction that never commits, so its database writes are invisible to every other test.

@pytest.fixture def db_session(): tx = connection.begin() yield session tx.rollback()

with pytest.raises(ExceptionType, match="text"): ... · mock.side_effect = Error(...)

Asserts an error actually happens, and forces a mocked dependency to fail on demand to test error-handling paths.

with pytest.raises(ValueError, match="positive"): validate_amount(-10)

testingpytesterror-handling
Testing failure scenarios

Python Packaging

10

python -m venv .venv · pip install <package>

Creates an isolated environment, then installs a package into whichever environment is active.

python -m venv .venv .venv\Scripts\activate pip install requests

packagingpipvenv
pip and venv

[build-system] · [project] · src/mypackage/

One TOML file for build config and metadata; src/ layout is the modern recommended directory shape.

[project] name = "mypackage" version = "0.1.0"

python -m build → dist/*.whl + dist/*.tar.gz

Runs the build backend to produce a wheel and a source distribution from pyproject.toml.

pip install build python -m build

packagingbuildwheel
Build systems and wheels

pip install -e .

Links a project's source directly into the current environment — edits take effect immediately, no reinstall.

pip install -e . python -c "import mypackage"

pkg==1.2.3 (pin) · pkg>=1.2,<2.0 (range) · pkg~=1.2 (compatible release)

A pin locks to one exact version; a range accepts any version within a window.

dependencies = ["requests>=2.31,<3.0"]

MAJOR.MINOR.PATCH

PATCH = bug fix, MINOR = compatible new feature, MAJOR = breaking change.

2.31.0 -> 2.31.1 (patch) -> 2.32.0 (minor) -> 3.0.0 (major)

packagingversioningsemver
Semantic versioning

pip install --index-url <private-url> <package>

Installs from a private package registry instead of public PyPI.

pip install --index-url https://internal.example.com/simple/ mypackage

python -m build → twine check dist/* → twine upload dist/*

Build, validate, then upload artifacts to PyPI or a private index.

python -m build twine upload dist/*

packagingpublishingpypi
Publishing packages

setup.py (executed) vs. pyproject.toml (parsed, not executed)

pyproject.toml is static data read without running any code; setup.py is Python code pip must execute.

[project] name = "mypackage" version = "0.1.0"

packagingpyproject.tomlsetup.py
Why pyproject.toml over legacy setup.py

Dependency and Environment Management

8

python -m venv .venv (packages) · uv python install 3.12 (interpreter)

A venv isolates packages; a version manager isolates which Python interpreter is even available.

uv python install 3.12 uv venv --python 3.12

pip install . (prod) · pip install ".[dev]" (prod + dev)

[project.optional-dependencies] dev = [...] adds dev-only packages on top of the base dependencies.

[project.optional-dependencies] dev = ["pytest>=8.0", "ruff>=0.5"]

dependenciespackagingproduction
Development vs. production dependencies

from dotenv import load_dotenv; load_dotenv()

Loads a .env file's NAME=value lines into os.environ — must run before reading any of those variables.

load_dotenv() db_url = os.environ.get("DATABASE_URL")

os.environ.get("API_KEY") → raise if missing

Read a secret from an environment variable and fail loudly and immediately if it is not set.

api_key = os.environ.get("API_KEY") if not api_key: raise RuntimeError("API_KEY must be set")

secretssecurityenvironment
Secrets

uv lock → uv.lock → uv sync

Resolves every dependency to an exact version and hash, then installs precisely what was locked.

uv lock uv sync

dependencieslock-filereproducibility
Lock files and reproducible environments

uv venv · uv pip install <pkg> · uv lock · uv sync

A faster, all-in-one alternative to pip + venv + a separate lock-file tool.

uv venv uv pip install requests

pipuvtools
pip and uv

poetry add <pkg> · pip-compile requirements.in

Poetry is an all-in-one manager; pip-tools adds just a lock step to plain pip.

poetry add requests # or: pip-compile requirements.in

poetrypip-toolsdependency-management
Poetry and pip-tools

conda create -n myenv python=3.12 · pyenv install 3.12

Conda handles Python plus non-Python dependencies; pyenv manages multiple Python interpreter versions.

conda create -n myenv python=3.12 conda activate myenv

condapyenvpython-version
Conda and pyenv (when relevant)

Code Quality and Tooling — Formatting and linting

3

black <file> · isort <file> · black --check <file>

Automatically reformat code (black) and sort imports (isort) to a consistent style.

black myfile.py isort myfile.py

code-qualityblackisortformatting
PEP 8 and code formatters (Black, isort)

ruff check <file> · flake8 <file>

Both lint for bugs and style issues — Ruff is faster and all-in-one; Flake8 relies on separate plugins.

ruff check --fix myfile.py

code-qualityruffflake8linting
Ruff and Flake8

pre-commit install → git commit (hooks run automatically)

Registers a real git hook that runs configured checks before every commit is allowed to complete.

pip install pre-commit pre-commit install

code-qualitypre-commitgit-hooks
Pre-commit

Code Quality and Tooling — Static analysis

2

mypy <file> · pyright <file>

Both statically check type hints for errors, without running any code — same syntax, different output format.

mypy myfile.py pyright myfile.py

code-qualitymypypyrighttype-checking
mypy and pyright

bandit myfile.py · bandit -r .

Scans source code for known vulnerability patterns — shell injection, hardcoded secrets — with severity ratings.

bandit -r . --severity-level high

code-qualitysecuritybandit
Security linters

Code Quality and Tooling — Engineering practices

3

def clear_name(typed: Arg) -> ReturnType: """Why, not what."""

Clear naming, type annotations, and a docstring explaining non-obvious behavior make code fast to review and maintain.

def calculate_shipping_cost(order: Order) -> Decimal: """Free above the $50 threshold."""

Refactor: structure changes, behavior does not — tests prove it

Keep tests green before and after a refactor; never mix structural and behavioral changes in one commit.

assert calculate_total_v1(items) == calculate_total_v2(items)

code-qualityrefactoringtechnical-debt
Refactoring safely and technical debt management

Performance — Fundamentals

3

O(1) < O(log n) < O(n) < O(n log n) < O(n²)

The shape of how an algorithm's cost grows as input size grows — smaller is better at scale.

x in a_set # O(1) x in a_list # O(n)

x in a_list # O(n) · x in a_set # O(1) average

List membership scans every item; set/dict membership hashes directly to the answer.

blocked_ids = set(blocked_id_list) # convert once if user_id in blocked_ids: ...

performancedata-structuresbig-o
Choosing the right data structure

Performance — Profiling tools

3

timeit.timeit(stmt, setup="", number=1000000)

Runs a small snippet repeatedly and returns total time, averaging out single-run noise.

timeit.timeit("x in s", setup="s = {1,2,3}", number=10000)

performancetimeitprofiling
timeit and microbenchmarks

cProfile.run("main()", "out.prof") → pstats.Stats("out.prof").sort_stats("cumulative")

Profiles a whole program's function calls, then analyzes and prints the results sorted by cost.

python -m cProfile -s cumulative myapp.py

performancecprofilepstatsprofiling
cProfile and pstats

tracemalloc.start() → get_traced_memory() → tracemalloc.stop()

Tracks real memory allocations and reports current/peak usage in bytes.

tracemalloc.start() current, peak = tracemalloc.get_traced_memory()

performancetracemallocmemory-profiling
tracemalloc, memory profilers, py-spy, and APM

Performance — Optimization

4

@functools.lru_cache(maxsize=None)

Caches a pure function's results by argument — repeated calls with the same arguments return instantly.

@lru_cache(maxsize=None) def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)

performancecachinglru_cache
Caching and lazy evaluation

WHERE id IN (...) · select_related()/prefetch_related() · connection pool

Fewer, larger round trips beat many small ones — watch for N+1 queries hidden inside a loop.

users = db.query("SELECT * FROM users WHERE id IN (?)", user_ids)

performancebatchingconnection-poolingdatabase
Batching, connection pooling, and query optimization

ProcessPoolExecutor (CPU-bound) · asyncio.gather (I/O-bound)

Parallelize genuinely CPU-bound work across cores; overlap I/O-bound waits on one thread with async.

with ProcessPoolExecutor() as ex: results = list(ex.map(fn, items))

performanceparallelizationasync-ioserialization
Parallelization, async I/O, and serialization optimization

Profile → find the real bottleneck → optimize → re-profile

Never optimize based on intuition alone — a profiler measures what actually dominates runtime.

python -m cProfile -s cumulative myapp.py

performanceprofilingoptimization
Measure first, optimize second

Memory Optimization

10

sys.getsizeof(container) — the container only, not what it references

Lists (and dicts/sets) over-allocate to amortize growth — real size grows in jumps, not linearly.

lst = [] for i in range(1000): lst.append(i) # occasional resizes, not every append

sum(x for x in data) — no brackets, no intermediate list

Dropping list-comprehension brackets to a generator expression avoids building an intermediate list for any one-pass reduction.

total = sum(x * x for x in huge_dataset)

class Row: __slots__ = ("id", "value")

Worth adding to classes instantiated in bulk — the per-instance saving multiplies into a real total.

__slots__ = ("id", "value") # add for a class with 100,000+ instances

memoryslotsoptimization
__slots__, at scale

copy.copy(x) (shallow, cheap) vs. copy.deepcopy(x) (deep, costs real memory)

A deep copy duplicates every nested object — genuinely more memory than a shallow copy, which shares them.

shallow = copy.copy(x) # nested objects shared deep = copy.deepcopy(x) # nested objects duplicated

snapshot2.compare_to(snapshot1, "lineno")

Diffs two tracemalloc snapshots, ranking which lines allocated the most new memory between them.

snapshot1 = tracemalloc.take_snapshot() # ... suspected leaking code ... snapshot2.compare_to(snapshot1, "lineno")

memoryleakstracemallocdebugging
Memory leaks, in practice

weakref.WeakValueDictionary()

A cache whose entries vanish automatically once nothing else references the value — the fix for a cache that should not keep everything alive.

cache = weakref.WeakValueDictionary() cache["key"] = some_object

@lru_cache(maxsize=1000) — bounded, not None

An unbounded cache (maxsize=None) grows forever with distinct arguments — bound it, or key on something that repeats.

@lru_cache(maxsize=1000) def compute(x): ...

memorycachinglru_cache
Caching side effects

[obj for obj in gc.get_objects() if isinstance(obj, SuspectedClass)]

Counts live instances of a suspected class right now — the concrete confirmation step in a memory investigation.

leaked = [o for o in gc.get_objects() if isinstance(o, Connection)] print(len(leaked))

memoryprofilingdebuggingtracemalloc
Memory profiling, in practice

Logging and Observability

5

logging.getLogger(__name__).setLevel(logging.INFO)

Gets a module-scoped logger and sets the minimum severity it will accept — a handler can filter further.

logger = logging.getLogger(__name__) logger.warning('disk usage at %d%%', 91)

loggingobservabilityproduction
Logging levels, handlers, and formatters

logger.info(msg, extra={"field": value})

Attaches a custom named field to one log record, readable back via record.<field> in a formatter.

logger.info('order created', extra={'order_id': 4821})

loggingobservabilityjson
Structured and JSON logging

logging.LoggerAdapter(logger, {"request_id": rid})

Wraps a logger so every call through it automatically carries the same extra context, like a request ID.

req_log = logging.LoggerAdapter(logger, {'request_id': rid}) req_log.info('processing')

tracer.start_as_current_span(name)

Starts a span and makes it the active context — any span started inside the with-block nests under it automatically.

with tracer.start_as_current_span('handle-request'): ...

observabilitytracingopentelemetrymetrics
Metrics and distributed tracing

GET /healthz (liveness) vs. GET /readyz (readiness)

Liveness answers "restart me or not"; readiness answers "route traffic to me or not" — never conflate the two.

def readiness(): return ({'status': 'ok'}, 200) if db_ok() else ({'status': 'unavailable'}, 503)

observabilityhealth-checkskubernetesreliability
Health, readiness, and liveness checks

Web and HTTP Fundamentals — HTTP

5

GET/HEAD/OPTIONS = safe · GET/PUT/DELETE/HEAD/OPTIONS = idempotent

The safety/idempotency matrix that decides whether a request is safe to auto-retry.

client.put(url, json=full_state) # idempotent, safe to retry as-is

httprestapi
HTTP methods

from http import HTTPStatus

Stdlib enum giving every standard status code a name, int value, and phrase — use the name, not a bare literal.

return {'id': new_id}, HTTPStatus.CREATED

headers={'Authorization': f'Bearer {token}'}

Headers are case-insensitive metadata sent alongside a request/response — never put secrets in the URL instead.

client.get(url, headers={'Accept': 'application/json'})

httpheadersapi
HTTP headers

cookie['name'] = value; cookie['name']['httponly'] = True

A cookie set with HttpOnly + Secure + SameSite is the baseline-safe way to hand a browser a session ID.

c['session_id']['samesite'] = 'Lax'

httpcookiessessionssecurity
Cookies and sessions

headers={'If-None-Match': etag}

A conditional request — the server returns 304 with no body if the resource has not changed since that ETag.

resp = client.get(url, headers={'If-None-Match': cached_etag}) resp.status_code # 304 if unchanged

4

Access-Control-Allow-Origin: <specific-origin>

The core CORS response header — a browser only lets cross-origin JS read the response if this matches its own origin.

return {'Access-Control-Allow-Origin': 'https://app.example.com'}

secrets.compare_digest(submitted_token, expected_token)

Constant-time CSRF token comparison — never use == for a security-sensitive token comparison.

if not secrets.compare_digest(form_token, session_token): abort(403)

html.escape(untrusted_value)

Converts HTML-significant characters to entities so untrusted input renders as inert text, not executable markup.

safe = html.escape(user_comment) return f'<p>{safe}</p>'

scheme + host + port = origin

The same-origin policy compares all three — differ in any one and the browser treats it as a different origin.

httpx.Client(verify=True) # default -- never disable cert verification in production

API Development

9

POST /orders/{id}/cancellation, not POST /cancelOrder

A non-CRUD action modeled as a sub-resource stays addressable (GET-able) later — a verb-shaped endpoint is a dead end.

routes[('GET', '/users/{user_id}/orders')] = list_orders_for_user

class Model(BaseModel): field: int = Field(gt=0)

Pydantic validates on construction and raises a field-level ValidationError — the standard shape for request/response contracts.

model.model_dump() # explicit, controlled serialization

apivalidationserializationpydantic
Request validation and response serialization

{"error": {"code": "...", "message": "..."}}

One consistent error envelope across every endpoint — a client branches on code, reads message, never parses a stack trace.

return {'error': {'code': 'order_not_found', 'message': '...'}}, 404

apierrorsdesign
Error responses

?page=2&page_size=20&sort=-created_at&status=paid&q=dragon

One collection endpoint, four independent query-parameter mechanics — pagination, sorting, filtering, and search.

total_pages = (total_items + page_size - 1) // page_size

/v1/orders vs. /v2/orders

URL-path versioning is the most visible, cache-friendly strategy — only endpoints with an actual breaking change need a new version.

VERSIONED_HANDLERS = {'v1': get_order_v1, 'v2': get_order_v2}

apiversioningdesign
API versioning

headers={"Idempotency-Key": uuid4()}

A client-generated key that lets the server safely dedupe a retried non-idempotent (POST) request.

if key in seen: return seen[key] # else process and store

apiidempotencyrate-limitingreliability
Idempotency keys and rate limiting

order.owner_id == user.id or user.is_admin

Object-level authorization — checked per action, against the SPECIFIC resource, not just "is this user logged in."

if not can_delete_order(user, order): return 403

model.model_json_schema()

Generates an OpenAPI-compatible JSON Schema directly from the same model used for request validation — one source of truth.

CreateOrderRequest.model_json_schema()['required']

Additive = safe · Removed/renamed/retyped/narrowed = breaking

The concrete test for whether an API change can ship on the existing endpoint or needs a new version.

estimated_delivery: str | None = None # safe: new, optional

apiversioningdesign
Backward compatibility

Redis

9

r.set(key, value) / r.get(key) / r.incr(key) / r.incrby(key, n)

set/get store and retrieve a string value under a key; incr/incrby atomically update an integer-valued key.

r.set("page_views", 1) r.incrby("page_views", 5) r.get("page_views") # '6'

r.rpush(key, *values) / r.lpop(key) / r.lrange(key, start, stop)

rpush/lpush push onto the tail/head; lpop/rpop remove from the head/tail; lrange(key, 0, -1) reads the full list in order.

r.rpush("job_queue", "task:a") r.blpop("job_queue", timeout=1)

redislistsqueue
Lists

r.sadd(key, *m) / r.sismember(key, m) / r.zadd(key, {m: score}) / r.zrevrange(key, 0, n, withscores=True)

Sets track unique unordered members with O(1) checks; sorted sets add a per-member score and stay readable in score order.

r.zadd("leaderboard", {"bob": 250}) r.zrevrange("leaderboard", 0, 0, withscores=True)

redissetssorted-setsleaderboard
Sets and sorted sets

r.hset(key, mapping={...}) / r.hget(key, field) / r.hgetall(key) / r.hincrby(key, field, n)

Hashes store field-value pairs under one key — the shape for "one object, several named fields" without JSON-encoding it into a string.

r.hset("user:1001", mapping={"logins": 4}) r.hincrby("user:1001", "logins", 1)

redishashes
Hashes

r.expire(key, seconds) / r.ttl(key) / r.persist(key) / r.set(key, value, ex=seconds)

expire/ttl set and read a countdown on a key; persist removes it; set(..., ex=) writes the value and TTL together.

r.set("otp:42", "914213", ex=300) r.ttl("otp:42") # 300

redisttlexpirationcache
TTL and expiration

r.pipeline(transaction=True) → pipe.watch(key) / pipe.multi() / pipe.execute()

pipeline() batches commands in one round trip; transaction=True wraps them in MULTI/EXEC; watch(key) aborts with WatchError if the key changed first.

with r.pipeline() as pipe: pipe.watch("stock:sku9") ... pipe.multi(); pipe.set(...); pipe.execute()

redistransactionsconcurrency
Atomic operations

r.publish(channel, msg) / pubsub.subscribe(channel) — r.xadd(stream, fields) / r.xreadgroup(group, consumer, {stream: ">"})

Pub/Sub broadcasts live with no history; Streams append a durable, ID-ordered log consumer groups can read and replay.

r.xadd("events:orders", {"order_id": "1001"}) r.xrange("events:orders", "-", "+")

redispubsubstreamsmessaging
Pub/Sub and Streams

r.set(key, token, nx=True, px=ttl_ms) / r.register_script(release_lua)(keys=[key], args=[token])

nx=True acquires only if absent; a Lua script checks the token and deletes atomically so you never release a lock you no longer hold.

token = str(uuid.uuid4()) r.set("lock:job:5", token, nx=True, px=5000)

redislocksconcurrencydistributed-systems
Distributed locks

r.get(key) → miss → compute value → r.set(key, value, ex=ttl_seconds)

Cache-aside: read from Redis first, fall back to the real lookup on a miss, and always write the cached copy with a TTL.

r.set("cache:user_profile:1001", value, ex=300)

rediscachingperformance
Caching

Caching

9

@functools.lru_cache(maxsize=128)

Caches a function's return value per distinct argument, in the current process's memory, evicting least-recently-used entries once maxsize is reached.

@lru_cache(maxsize=128) def slow_square(x): return x * x

cachinglru_cachein-memoryperformance
Why caching is needed, and in-memory cache

redis.Redis(host=..., port=6379).set(key, value, ex=seconds)

Connects to a Redis server and stores a key with an optional TTL, shared across every process that connects to the same server.

r = redis.Redis(host="localhost", port=6379) r.set("user:42", "Alice", ex=300)

cachingredisdistributed
Redis cache

cache-aside · write-through · write-back

The three patterns for when a cache is populated relative to a database write — lazily on read miss, together with the write, or write-now-flush-later.

if key in cache: return cache[key] value = db[key]; cache[key] = value # cache-aside

cachingcache-asidewrite-throughwrite-back
Cache-aside, write-through, and write-back

expires_at = time.monotonic() + ttl_seconds

A TTL cache stores an absolute expiry deadline at insert time and checks it on every read; invalidation instead removes the entry directly, tied to a write.

if time.monotonic() > expires_at: del store[key] # treat as a miss

cachingttlinvalidationexpiry
TTL and cache invalidation

with lock: if key in cache: return cache[key] ...rebuild...

Prevents a cache stampede by letting only the first thread for a key rebuild it, while others wait on the same lock and re-check before rebuilding again.

lock = locks.setdefault(key, threading.Lock()) with lock: if key in cache: return cache[key]

cachingstampedethreadinglockconcurrency
Cache stampede

warm_cache_on_startup()

Pre-populates a cache with expected hot keys before serving traffic, avoiding a burst of simultaneous misses right after a deploy or restart.

def warm_cache_on_startup(): for key, value in load_top_products().items(): cache[key] = value

cachingwarmingstartupcold-start
Cache warming

RedisCluster(host=..., port=...).set(key, value, ex=seconds)

A distributed cache client that routes each key to the correct node via consistent hashing, spreading data and load across multiple machines.

rc = RedisCluster(host="localhost", port=6379) rc.set("user:42", "Alice", ex=300)

cachingdistributedredis-clustersharding
Distributed caching

strong consistency vs. eventual consistency

How closely a cached value tracks the source of truth — strong means always fresh (expensive), eventual means fresh within a bounded window (cheaper).

# write-through: near-strong db[key] = value; cache[key] = value

cachingconsistencystalenesseventual-consistency
Cache consistency

try: cache_get(key) except CacheUnavailable: return db[key]

Fail-open pattern: on a cache-specific error, fall back to the source of truth so the cache being down degrades latency, not correctness.

try: return cache_get(key) except CacheUnavailable: return db[key]

cachingfailurefail-openresilience
Cache failure behavior

Background Jobs and Task Queues

8

Celery(name, broker="redis://...") | Queue(connection=Redis()) | actor (dramatiq)

Celery and RQ both wrap a broker connection and a decorator that turns a function into a background task; Dramatiq uses an "actor" decorator with a similar shape.

app = Celery("tasks", broker="redis://localhost:6379/0") @app.task def my_task(x, y): return x + y

background-jobsceleryrqdramatiqredisrabbitmqkafka
Task queue tools landscape

queue.put(job) → worker: job = queue.get() → process → queue.task_done()

A producer enqueues; a worker dequeues, runs, and acknowledges — the four stages every task queue library automates over a real broker.

work_queue.put({"id": 1}) job = work_queue.get() ... work_queue.task_done()

background-jobsqueueworker
Job queues and workers

ThreadPoolExecutor(max_workers=N) | ProcessPoolExecutor(max_workers=N) | celery worker --concurrency=N

The number of jobs run at once — threads/async for I/O-bound jobs, processes for CPU-bound jobs.

with ThreadPoolExecutor(max_workers=4) as pool: results = list(pool.map(do_work, jobs))

background-jobsconcurrencyworkers
Worker concurrency

delay = base_delay * (2 ** (attempt - 1))

Exponential backoff: each retry waits roughly twice as long as the last, up to a max_attempts cap that eventually gives up.

for attempt in range(1, max_attempts + 1): try: return fn() except RuntimeError: if attempt == max_attempts: raise time.sleep(base_delay * (2 ** (attempt - 1)))

background-jobsretriesbackoff
Retries and exponential backoff

if attempt == max_attempts: dead_letter_queue.append({"job": job, "error": str(exc)})

Route a job to a separate dead-letter queue once it exhausts its retries, instead of dropping it or retrying forever.

for attempt in range(1, max_attempts + 1): try: handler(job) break except ValueError as exc: if attempt == max_attempts: dlq.append({"job": job, "error": str(exc)})

background-jobsdlqretries
Dead-letter queues

task.apply_async(countdown=seconds) | task.apply_async(eta=datetime) | beat_schedule (cron-style)

countdown/eta delay a single call once; a beat/cron-style schedule repeats a task on an interval.

send_welcome_email.apply_async(args=[user_id], countdown=600)

background-jobsschedulingdelayed-jobscelery
Job scheduling and delayed jobs

if idempotency_key in processed: return processed[idempotency_key]

Check a stored idempotency key before doing the job's real work, so a redelivered (duplicate) job returns the original result instead of repeating a side effect.

def charge_card(idempotency_key, amount): if idempotency_key in processed_payments: return processed_payments[idempotency_key] result = {"status": "charged", "amount": amount} processed_payments[idempotency_key] = result return result

background-jobsidempotencyat-least-onceduplicates
At-least-once delivery, duplicate processing, and idempotency

job_states[job_id] = {"status": ..., "error": ...}

Record a job's status and error on every transition, then filter for the failed state to find what needs recovery.

try: fn() job_states[job_id]["status"] = "succeeded" except Exception as exc: job_states[job_id]["status"] = "failed" job_states[job_id]["error"] = str(exc)

background-jobsmonitoringfailure-recovery
Job monitoring and failure recovery

Architecture and Design Patterns — Common architecture styles

3

presentation -> service -> repository -> database

Layered stacks responsibility top-down; clean/hexagonal instead make every outer layer depend inward on the business rules through an interface.

class OrderService: def __init__(self, repository): self.repository = repository

architecturelayeredclean-architecturehexagonal
Layered, modular monolith, clean, and hexagonal architecture

value object (no identity) · entity (has identity) · aggregate (root + members)

DDD names the business's own concepts directly in code, and puts every rule about an aggregate behind its single root object.

order.add_line("sku-1", 2, unit_price) # raises inside the aggregate root if invalid

dddarchitecturedomain-model
Domain-driven design fundamentals

bus.publish(event_name, payload) · bus.subscribe(event_name, handler)

A publisher emits an event by name; any number of subscribed handlers run without the publisher referencing them.

bus.subscribe("order_placed", update_inventory) bus.publish("order_placed", {"order_id": 501})

microservicesevent-drivenarchitecturepub-sub
Microservices and event-driven architecture

Architecture and Design Patterns — Patterns

10

def x_factory(kind): return {"a": A, "b": B}[kind]()

A Factory hides "which class?" behind one function; an Abstract Factory hides it for a whole matching family of classes.

notifier = notifier_factory("email") notifier.send("your order shipped")

design-patternsfactoryabstract-factorycreational
Factory and Abstract Factory

Builder().field1(x).field2(y).build()

Each builder method sets one field and returns self; build() assembles and validates the finished object.

HttpRequestBuilder().method("POST").url("/orders").build()

design-patternsbuildercreational
Builder

class Adapter(Target): def __init__(self, adaptee): self.adaptee = adaptee

Adapter implements the interface callers expect and translates each call to the wrapped, incompatible object internally.

adapter = LegacyXmlAdapter(LegacyXmlParser()) print_items(adapter) # print_items only knows JsonDataSource

design-patternsadapterstructural
Adapter

self.strategy = strategy; self.strategy.calculate(...)

The context class holds an interchangeable strategy object and delegates to it, instead of branching on a type flag itself.

cart = Cart(StandardShipping()) cart.shipping_strategy = ExpressShipping() # swap at runtime

design-patternsstrategybehavioral
Strategy

subject.attach(observer) · subject.notify(event)

Observers register with a subject and are called back through a shared interface whenever the subject notifies — decoupled from each other and from the subject.

ticker.attach(PriceLogger()) ticker.set_price("ACME", 120)

design-patternsobserverbehavioral
Observer

class XCommand(Command): def execute(self): ... def undo(self): ...

A command bundles a request (and its reverse) as an object the invoker can store, run, and undo without knowing what it does.

remote.submit(TurnOnCommand(light)) remote.undo_last()

design-patternscommandbehavioral
Command

class AddOn(Base): def __init__(self, wrapped): self._wrapped = wrapped

A decorator wraps an object of the same interface and delegates to it, adding behavior before or after — stackable without one class per combination.

order = WithCaramel(WithMilk(Espresso())) order.cost() # 3.75

design-patternsdecoratorstructural
Decorator (structural pattern)

repo.get_by_id(id) · repo.add(entity)

A repository exposes get/add/list methods over an aggregate, hiding whether storage is SQL, an API, or memory behind one interface.

repo = InMemoryUserRepository() repo.add({"id": 1, "name": "Priya Shah"}) repo.get_by_id(1)

design-patternsrepositorydata-access
Repository

class XService: def __init__(self, repository): ... def do_the_use_case(self, ...): ...

A service class represents one use case and coordinates repositories/domain objects for it — the single place that logic lives.

service = OrderPlacementService(ProductRepository()) service.place_order("sku-1", 2)

design-patternsservice-layerarchitecture
Service Layer

def __init__(self, dependency): self.dependency = dependency

A class receives its dependencies as constructor arguments rather than constructing them itself — see the roadmap's own Dependency Injection section for the full treatment.

ReportGenerator(ConsoleLogger()).generate()

design-patternsdependency-injection
Dependency Injection (pattern catalog entry)

Dependency Injection — Dependency Injection

7

class Sub(ABC): @abstractmethod def method(self): ...

A high-level class should depend on an abstraction (an ABC or a duck-typed shape), never on one concrete implementation it builds itself.

class OrderService: def __init__(self, mailer: Mailer): self.mailer = mailer

dependency-injectiondesignabc
Dependency inversion

def __init__(self, dependency): self.dependency = dependency

Accept a dependency as a constructor parameter and store it on self, rather than constructing it inside __init__.

service = UserService(Repository()) fake_service = UserService(FakeRepository())

dependency-injectionconstructorstesting
Constructor injection

def do_thing(data, dependency=default_dependency): return dependency(data)

Accept a dependency (often a function) as a parameter with a sensible default, so a caller can override it per call without editing the function.

total_price([10, 20, 30], flat_tax) send_email(to, subject, sender=fake_sender)

dependency-injectionfunctionstesting
Function injection

container.resolve("service_name")

Ask a container for a fully wired-up dependency by name; the container builds it and everything it depends on, caching the result.

container = Container() user_service = container.resolve("user_service")

dependency-injectionarchitecturecontainer
Dependency containers

def endpoint(dep=Depends(get_dependency)): ...

Mark a FastAPI endpoint parameter as a dependency to resolve before the endpoint runs; the endpoint receives the dependency's return value as a plain argument.

def read_items(session=Depends(get_db_session)): return query(session)

dependency-injectionfastapiweb
FastAPI dependency injection

service = ClassUnderTest(FakeDependency())

Inject a fake implementation of a dependency in a test so the class under test runs without a real network call, database, or side effect.

fake_gateway = FakePaymentGateway() service = OrderService(fake_gateway) assert fake_gateway.charged == [49.99]

dependency-injectiontestingfakes
Testing with injected dependencies

def fn(explicit_dependency, ...): ...

Pass every dependency a function actually uses as a parameter — a module-level global a function reads or mutates without declaring it cannot be swapped or tested in isolation.

def fetch_user(pool, user_id): return pool.fetch_user(user_id)

dependency-injectiontestingglobals
Avoiding hidden global dependencies

Linux

8

os.getpid() · os.getppid() · subprocess.run([...])

A process is an isolated, independently-numbered unit of execution; a thread runs inside one process and shares its memory.

import os print(os.getpid(), os.getppid())

linuxprocesspidthreados
Processes, PIDs, and threads

signal.signal(signal.SIGTERM, handler)

Registers a Python function to run when the named signal arrives, replacing its default action. SIGKILL and SIGSTOP cannot be caught.

def handler(signum, frame): shutdown_requested = True signal.signal(signal.SIGTERM, handler)

linuxsignalsigtermsigkillgraceful-shutdown
Signals

os.chmod(path, 0o600) · Path(path).chmod(0o755) · stat.filemode(mode)

File permissions are three octal-encoded triads (owner/group/other) of read(4)/write(2)/execute(1); every file also has an owning user and group.

import stat stat.filemode(stat.S_IFREG | 0o600) # "-rw-------"

linuxchmodpermissionsusersgroupsstat
File permissions, users, and groups

os.environ.get("KEY", default) · os.environ["KEY"] · PurePosixPath("/etc") / "app"

Environment variables carry per-process configuration; the filesystem follows a conventional layout (/etc config, /var/log logs, /tmp scratch, /home user files).

db_host = os.environ.get("DB_HOST", "localhost")

linuxenvironment-variablesos.environfilesystempathlib
Environment variables and the filesystem

ps aux · top · htop · lsof -i :PORT · kill -9 PID

ps/top/htop inspect running processes and resource use; lsof lists a process's open files and network connections; kill sends it a signal.

subprocess.run(["lsof", "-i", ":8000"])

grep -c PATTERN file · awk -F',' '{print $2}' file · sed 's/OLD/NEW/g' file

grep filters lines by pattern, awk extracts/processes fields, sed rewrites text — chainable with a pipe for quick log analysis without leaving the shell.

grep ERROR app.log | awk '{print $1}'

linuxgrepawksedclitext-processing
Text processing: grep, awk, sed

curl -s URL · curl -sI URL · curl -o FILE URL · curl -w "%{http_code}" -o /dev/null URL

curl sends an HTTP request from the shell and is built for scripting around the response; wget defaults to resumable, retrying downloads to disk.

subprocess.run(["curl", "-s", "-w", "%{http_code}", "-o", "/dev/null", url])

linuxcurlwgethttpcli
curl and wget

ssh user@host · scp file user@host:/path/ · systemctl restart NAME.service · crontab -e

ssh/scp connect to and move files onto a remote Linux host; systemd keeps a service running and restarts it on crash; cron runs a command on a schedule.

subprocess.run(["ssh", "deploy@host", "systemctl", "is-active", "myapp.service"])

Networking

9

(ip_address, port)

A network endpoint is an (IP address, port) pair -- the IP finds the host, the port finds the process.

ipaddress.ip_address('192.168.1.10').is_private # True

networkingtcp-ipip-addressports
TCP/IP, IP addresses, and ports

socket.socket(family, type) -> bind/listen/accept (server) or connect (client) -> send/recv

The stdlib socket module's core object for one end of a TCP or UDP connection.

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('example.com', 80))

networkingsocketstcp
Sockets

socket.SOCK_STREAM (TCP) vs socket.SOCK_DGRAM (UDP)

TCP guarantees ordered, complete delivery after a handshake; UDP sends immediately with no guarantees.

udp.sendto(b'ping', (host, port)) # no connect() needed

networkingtcpudp
TCP vs UDP

socket.gethostbyname(host) / socket.getaddrinfo(host, port)

Resolves a hostname to an IP address via the OS DNS resolver.

socket.gethostbyname('www.python.org') # '167.82.56.223'

networkingdns
DNS

TCP handshake -> TLS handshake (HTTPS only) -> HTTP request/response

HTTP is application-layer over TCP; HTTPS inserts a TLS handshake between the TCP connection and the first HTTP bytes.

ssl.create_default_context().wrap_socket(tcp_sock, server_hostname=host)

client -> reverse proxy / load balancer -> backend instance(s)

A reverse proxy forwards requests to backend server(s), hiding them from the client; a load balancer additionally distributes those requests across multiple instances.

nginx upstream block + proxy_pass, or HAProxy backend + balance roundrobin

networkingreverse-proxyload-balancerinfrastructure
Reverse proxy and load balancer

forward proxy: client-configured intermediary · NAT: router-level address rewriting

A proxy is chosen by the client at the application level; NAT is transparent IP-address translation at the network boundary.

httpx.Client(proxy='http://proxy.internal:8080')

networkingproxynat
Proxies and NAT

socket.settimeout(seconds)

Bounds how long a blocking socket call (connect, recv) waits before raising socket.timeout instead of hanging.

s.settimeout(5) s.connect((host, port)) # raises socket.timeout if too slow

networkingconnection-poolingtimeouts
Connection pooling and timeouts

WebSocket: Upgrade: websocket -> 101 · SSE: Content-Type: text/event-stream

WebSockets are full-duplex over an upgraded connection; SSE is a one-way, plain-HTTP event stream with built-in browser reconnect.

yield 'data: {"progress": 42}\n\n' # one SSE event

networkingwebsocketsssestreaming
WebSockets and Server-Sent Events (SSE)

ASGI, WSGI, Uvicorn and Gunicorn

6

WSGI: app(environ, start_response) → [bytes] ASGI: async def app(scope, receive, send) → None

WSGI is one blocking function per request; ASGI is one async function per connection that awaits and sends messages, adding WebSocket and lifespan support.

async def app(scope, receive, send): await receive() await send({"type": "http.response.start", "status": 200, "headers": []}) await send({"type": "http.response.body", "body": b"ok"})

wsgiasgiweb-servers
WSGI vs ASGI

uvicorn <module>:<attribute> [--host H] [--port P] [--workers N] [--reload]

Runs an ASGI app. --reload for development; --workers for basic multi-process production use; combine with Gunicorn for full process management.

uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

uvicornasgiweb-servers
Uvicorn

gunicorn -w N [-k WORKER_CLASS] [--bind ADDR] [--graceful-timeout S] module:app

Forks N worker processes and supervises them. Default worker class is sync WSGI; use uvicorn.workers.UvicornWorker to run an ASGI app.

gunicorn -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 main:app

gunicornwsgiweb-servers
Gunicorn

-w N (processes) · --worker-class {sync|gthread|UvicornWorker} · --threads N (gthread only)

Processes give real parallelism across CPU cores; threads help concurrent I/O-bound work within a process; an async worker's event loop handles many I/O-bound connections without extra threads.

gunicorn -w 9 -k uvicorn.workers.UvicornWorker main:app # (2*4 cores)+1, async worker

workersthreadsconcurrencygunicorn
Workers, worker processes, and threads

uvicorn --proxy-headers --forwarded-allow-ips <proxy-ip>

Puts Nginx/a load balancer in front of the app server for TLS termination, static files, and client buffering; trust forwarded headers only from that proxy's address.

uvicorn main:app --proxy-headers --forwarded-allow-ips 10.0.0.5

lifespan.startup / lifespan.shutdown · SIGTERM → drain (≤ graceful-timeout) → SIGKILL

Open shared resources on lifespan.startup, close them on lifespan.shutdown. A stop signal drains in-flight requests up to a configured timeout before the process is force-killed.

@asynccontextmanager async def lifespan(app): app.state.pool = await create_pool() yield await app.state.pool.close()

Monitoring and Production Operations

8

metrics / logs / traces → alerts, dashboards, SLOs, incident response

The three raw observability signals this section's tools (alerts, dashboards, SLOs, incident response) all consume — produced in Python as covered by Logging and Observability.

observabilitymetricslogstracesmonitoring
Metrics, logs, and traces (the ops layer)

GET /healthz, GET /readyz

The first check an on-call engineer or an uptime monitor makes — full liveness/readiness mechanics covered in Logging and Observability.

observabilityhealth-checksmonitoringincident-response
Health checks (in the ops loop)

IF metric compares-to threshold FOR duration THEN notify(channel)

The rule shape shared by Prometheus Alertmanager, CloudWatch Alarms, and Grafana alerting — a dashboard charts the same metric without the threshold or the page.

all(sample > threshold for sample in recent_samples)

monitoringalertsdashboardssre
Alerts and dashboards

error_budget = 1 - slo_target

SLI is the measured number, SLO is the target, error budget is 100% minus the SLO — negative remaining budget means the SLO is breached.

budget_remaining = error_budget - (1 - sli)

sresloslierror-budgetreliability
SLOs, SLIs, and error budgets

detect -> declare -> mitigate -> resolve -> analyze

The incident lifecycle, in order — mitigation always comes before full root cause analysis, which is modeled as a chain of contributing factors, not one cause.

if not mitigated: return "mitigate now"

sreincident-responseroot-cause-analysisreliability
Incident response and root cause analysis

summary, impact, timeline, root cause, what went well, action items (owner + status)

The standard blameless-postmortem sections from Google's SRE book — action items need an owner or the postmortem produces no real change.

[a for a in actions if a["status"] != "done"]

srepostmortemincident-responsereliability
Postmortems

try: return call_dependency() / except SpecificError: return fallback

Catch the specific dependency exception and return a fallback value — a circuit breaker adds "stop calling after N consecutive failures" on top of the same idea.

if self.failure_count >= self.failure_threshold: self.open = True

reliabilitygraceful-degradationcircuit-breakerexception-handling
Graceful degradation

if error_rate_breached(errors, total): return previous_version

A rollback reverts to the last known-good version — the fastest incident mitigation, since it does not require knowing the root cause first.

choose_active_version("v2.4.0", "v2.3.1", errors=80, total=1000) # -> "v2.3.1"

deploymentrollbackincident-responsereliability
Rollbacks

API Reliability

9

httpx.Client(timeout=httpx.Timeout(connect=2.0, read=5.0))

Bounds how long a single call may run before raising instead of hanging — set connect and read separately.

try: r = client.get(url) except httpx.TimeoutException: ... # decide next step yourself

reliabilitytimeoutsnetworking
Timeouts

time.sleep(random.uniform(0, min(cap, base * 2 ** (attempt - 1))))

Exponential backoff with jitter: wait grows each attempt, capped, and randomized within that cap.

for attempt in range(1, max_attempts + 1): try: return call() except TRANSIENT: if attempt == max_attempts: raise time.sleep(random.uniform(0, min(cap, base * 2 ** (attempt - 1))))

reliabilityretriesbackoffjitter
Retries, exponential backoff, and jitter

cb.call(downstream_fn, *args)

Routes a call through closed/open/half-open state — rejects immediately while open instead of attempting a known-failing downstream.

try: result = cb.call(fetch_from_downstream) except CircuitOpenError: result = fallback_value()

reliabilitycircuit-breakerresilience
Circuit breakers

ClientSideLimiter(capacity, refill_rate).allow()

A client-side token bucket that self-throttles outgoing calls to protect a downstream, rather than the server protecting itself.

if limiter.allow(): call_downstream() else: queue_or_skip()

reliabilityrate-limitingbackpressure
Rate limiting as a client-side reliability defense

if dedup_key in processed: return processed[dedup_key]

The check that makes a retried write safe: recognize an already-completed request by its key instead of repeating the side effect.

result = processed.get(key) or run_and_store(key)

reliabilityidempotencyretries
Idempotency as what makes a retry safe

pool.acquire() # raises when every connection is checked out

A finite, shared resource — what happens on exhaustion (fail fast vs. block) is a reliability decision, not an afterthought.

try: conn = pool.acquire() except TimeoutError: return degrade_gracefully()

reliabilityconnection-poolingresource-limits
Connection pooling as part of the resilience picture

if len(value) > MAX: raise ValueError(...)

A field-length or list-length ceiling enforced alongside type validation, so a well-typed request cannot still be unbounded.

if len(items) > MAX_ITEMS: raise ValueError(f"items exceeds limit of {MAX_ITEMS}")

reliabilityvalidationinput-limits
Request validation and input limits

Bulkhead(name, capacity).run(fn)

An isolated, per-dependency resource pool — exhausting one bulkhead does not affect any other dependency's separate pool.

reports_pool = Bulkhead("reports", capacity=5) checkout_pool = Bulkhead("checkout", capacity=20)

reliabilitybulkheadisolation
Bulkheads

except (ConnectionError, TimeoutError): return fallback_value()

Catch only the specific downstream-failure types and substitute a worse-but-working result, so the request still succeeds.

try: return live_value() except (ConnectionError, TimeoutError): logger.warning("using fallback") return fallback_value()

reliabilitygraceful-degradationfallback
Graceful degradation and fallbacks

Data Serialization

7

json.dumps(obj, indent=2) / json.loads(text)

Converts between Python objects and JSON text; raises TypeError on dumps for unsupported types, JSONDecodeError on loads for malformed text.

json.dumps({"user_id": 4821}, indent=2)

jsonserializationstdlib
JSON

pickle.dumps(obj) / pickle.loads(data)

Serializes almost any Python object to bytes; loads can execute arbitrary code — never call on untrusted data.

blob = pickle.dumps({"user_id": 4821, "roles": {"admin"}})

pickleserializationsecuritystdlib
Pickle — including security risks

csv.DictWriter(file, fieldnames=[...]) / csv.DictReader(file)

Reads and writes CSV rows as dicts keyed by column name, correctly quoting/unquoting fields that contain the delimiter.

list(csv.DictReader(open("users.csv")))

csvserializationstdlib
CSV

yaml.safe_dump(obj) / yaml.safe_load(text)

Third-party (PyYAML) round trip between Python objects and YAML text — always use the safe_ variants, never plain load/dump on untrusted input.

yaml.safe_load(open("config.yaml"))

yamlserializationpyyamlconfig
YAML

MessagePack (no schema) · Protocol Buffers (.proto, numbered fields) · Avro (JSON schema, name-resolved)

Three binary serialization formats outside the stdlib — pick by whether you need a schema at all, and if so, whether cross-language RPC or pipeline-style schema evolution matters more.

len(json.dumps(obj).encode()) vs. len(pickle.dumps(obj))

Serialization cost is CPU (encode/decode time) plus size (bytes produced) — both are measurable on your actual payload, never assumed.

len(json.dumps(payload).encode("utf-8"))

serializationperformancejsonpickle
Serialization/deserialization costs

record.get("new_field", default) — the read-side pattern for a safely evolved schema

Backward compatible = a new reader can still read old data. Add optional fields with defaults; avoid removing or renaming fields a live reader depends on.

schema-evolutionbackward-compatibilityavroprotobuf
Schema evolution and backward compatibility

CLI and Automation

8

sub = parser.add_subparsers(dest="command", required=True) sub.add_parser("name").set_defaults(func=handler)

Split one CLI into named subcommands, each with its own scoped arguments and handler function.

args = parser.parse_args(["migrate", "--target", "0007"]) args.func(args)

cliargparsesubcommands
CLI application structure

os.environ.get("KEY", default) · sys.exit(0 | nonzero)

Read configuration from the environment safely; report success or failure through the process exit code.

if api_key is None: sys.exit(1)

subprocess.run(cmd_list, check=True, capture_output=True, text=True)

Run an external command, raise on failure, and capture its output as text — the automation-safe defaults.

try: subprocess.run(["deploy.sh"], check=True) except subprocess.CalledProcessError as e: sys.exit(e.returncode)

clisubprocessshellautomation
Subprocesses and shell integration

sys.exit(0 if success else 1) # the only thing cron reads

A cron-safe script logs instead of printing, is idempotent, and signals outcome only through its exit code.

logging.exception("job failed") sys.exit(1)

clicronschedulingautomation
Scheduling and cron

processed, errors = 0, 0 # track a summary; exit_code = 0 if errors == 0 else 1

A maintenance script logs each action, supports --dry-run, and is idempotent — safe to run again after a partial failure.

logger.info("removed %s", item) # not print() -- nobody watches an unattended run

cliautomationmaintenanceidempotent
Automation scripts and maintenance scripts

for batch in chunked(fetch_records(), size=500): write_batch([migrate(r) for r in batch])

Transform existing data in batches, with a dry-run pass and a post-migration verification step.

climigrationdataautomation
Data migrations

for batch in chunked(items, size=500): process_batch(batch)

Process a large collection in fixed-size, independently retriable chunks instead of all at once.

batches = list(chunked(range(1, 11), 3)) # [[1,2,3], [4,5,6], [7,8,9], [10]]

clibatchautomation
Batch jobs

subprocess.run(step, check=True) # gate on exit code, not printed text

Chain deployment or dev-tooling steps, stopping at the first non-zero exit code rather than parsing output.

for step in [build_cmd, push_cmd, deploy_cmd]: subprocess.run(step, check=True)

clideploymenttoolingautomation
Deployment tooling and developer tooling

File and Resource Handling

6

open(path, "r", encoding="utf-8") · open(path, "rb") · Path(path).read_text(encoding=...) · Path(path).read_bytes()

"r"/"w" (text) decode/encode using an explicit encoding and return/accept str; "rb"/"wb" (binary) return/accept raw bytes with no encoding involved.

Path("notes.txt").read_text(encoding="utf-8")

for line in open(path): ... · while chunk := f.read(size): ...

Iterate a file line by line, or read it in fixed-size chunks, to process it without loading the whole file into memory at once.

with open(path, "rb") as f: while chunk := f.read(8192): process(chunk)

filesstreamingmemorygenerators
Streaming files and large-file processing

tempfile.TemporaryDirectory() · tempfile.NamedTemporaryFile(delete=False)

TemporaryDirectory() auto-deletes a scratch directory on with-block exit. NamedTemporaryFile(delete=False) keeps the file on disk after closing, for handing its path to another process or handle.

with tempfile.TemporaryDirectory() as td: scratch = os.path.join(td, "work.txt")

filestempfilecleanup
Temporary files

Path(p).chmod(0o644) · stat.S_IMODE(Path(p).stat().st_mode)

Set a file's permission bits with chmod(); read them back with stat.S_IMODE() to mask off the file-type bits st_mode also encodes.

stat.S_IMODE(Path("file.txt").stat().st_mode)

filespermissionschmodstat
File permissions

gzip.open(path, "wb"/"rt") · zipfile.ZipFile(path, "w") · tarfile.open(path, "w:gz")

gzip compresses one stream; zipfile/tarfile bundle multiple files into one archive, with tarfile's "w:gz" mode combining bundling and compression.

with tarfile.open("bundle.tar.gz", "w:gz") as tf: tf.add("notes.txt")

filesgzipzipfiletarfilecompression
Compression and archives

with open(path) as f: ...

with guarantees a resource's cleanup (__exit__, which closes a file) runs on every exit path from the block — success, early return, or exception.

with open(path) as f: data = f.read()

filescontext-managerscleanupwith
Resource cleanup

Date and Time

5

date(y, m, d) · time(h, mi, s) · datetime(y, m, d, h, mi, s) · timedelta(days=, hours=)

The four core datetime module types — a day, a clock reading, a combined instant, and a duration.

datetime.combine(date(2026, 8, 21), time(9, 30)) + timedelta(hours=1)

stdlibdatetimedatetimetimedelta
datetime, date, time, and timedelta

dt.astimezone(ZoneInfo("Region/City"))

Converts an aware datetime to a different named time zone without changing the instant it represents.

datetime.now(timezone.utc).astimezone(ZoneInfo("Europe/London"))

stdlibdatetimezoneinfotimezonesutc
Time zones, UTC, and zoneinfo

dt.tzinfo is None → naive · dt.tzinfo is not None → aware

Naive datetimes carry no time zone and cannot be safely compared to aware ones — always construct aware datetimes with an explicit zone.

datetime.now(timezone.utc) # aware, not datetime.now()

stdlibdatetimetimezonesnaiveaware
Naive vs. aware datetimes

datetime(..., fold=0|1, tzinfo=ZoneInfo(...))

fold disambiguates the repeated wall-clock hour created by a fall-back DST transition — fold=0 is earlier, fold=1 is later.

datetime(2026, 11, 1, 1, 30, fold=1, tzinfo=ZoneInfo("America/New_York"))

stdlibdatetimedstfoldtimezones
Daylight saving time transitions

dt.isoformat() / datetime.fromisoformat(s) · dt.timestamp() / datetime.fromtimestamp(ts, tz=timezone.utc)

The two standard round-trip formats for a datetime — ISO 8601 text and Unix timestamp numbers.

datetime.fromtimestamp(dt.timestamp(), tz=timezone.utc) == dt

stdlibdatetimeiso8601timestampserializationjson
ISO 8601, Unix timestamps, and date serialization

Networking and HTTP Clients

8

requests.get(url) | httpx.get(url) | await session.get(url)

requests: sync only. httpx: sync (Client) and async (AsyncClient) in one library, plus HTTP/2. aiohttp: async only.

async with httpx.AsyncClient() as client: r = await client.get(url)

httprequestshttpxaiohttpclient
requests, httpx, and aiohttp

HTTPAdapter(max_retries=Retry(total=3, status_forcelist=[...])), timeout=(connect, read)

Pool by reusing one Session/Client; bound wait time with an explicit timeout; retry via a mounted adapter (requests) or custom logic for status codes (httpx/aiohttp).

session.mount('https://', HTTPAdapter(max_retries=Retry(total=3))) session.get(url, timeout=(3.05, 10))

headers={'Authorization': f'Bearer {token}'} | auth=(user, pass) | auth=httpx.BasicAuth(user, pass)

Bearer auth is a plain header on any client; Basic auth has a dedicated auth= parameter per library.

session.headers.update({'Authorization': f'Bearer {token}'}) # applies to every request

httpauthheadersbearerbasic-auth
Authentication and headers

requests: iter_content(N) | httpx: iter_bytes(N) | aiohttp: content.iter_chunked(N)

Read a response body in fixed-size chunks instead of loading it whole — needed for large downloads.

with requests.get(url, stream=True) as r: for chunk in r.iter_content(8192): f.write(chunk)

httpstreamingdownloadmemory
Streaming responses

async with httpx.AsyncClient() as client: await asyncio.gather(*(client.get(u) for u in urls))

Reuse one AsyncClient/ClientSession and gather() several requests to run them concurrently — total time approaches the slowest single response, not the sum.

await asyncio.gather(*(client.get(u) for u in urls), return_exceptions=True)

httpasynchttpxaiohttpconcurrency
Async HTTP clients

response.raise_for_status()

No-op on 2xx; raises HTTPError (requests) or HTTPStatusError (httpx) on 4xx/5xx, with .response attached.

try: response.raise_for_status() except requests.exceptions.HTTPError as e: log(e.response.status_code)

httpstatus-codeexceptionserror-handling
HTTP status handling in a client

response.json() # raises JSONDecodeError on malformed body

A 2xx status does not guarantee a valid or expected-shape body — parse defensively and check required fields.

try: data = response.json() except requests.exceptions.JSONDecodeError: handle_bad_response()

httpjsonvalidationerror-handling
Response validation

httpx.Client(proxy="http://p:8080") · httpx.Client(verify="/path/ca.pem")

Route a client through a proxy with proxy=/proxies=; trust a specific CA bundle with verify= rather than disabling certificate verification with verify=False.

client = httpx.Client(verify="/etc/ssl/certs/internal-ca.pem")

Senior-Level Engineering Skills — Senior-Level Engineering Skills

12

ADR: Context -> Decision -> Consequences

An architecture review evaluates a written ADR before implementation, and a "blocker:" concern from any reviewer gates approval.

review.raise_concern("priya", "blocker: no plan for message ordering guarantees")

architecturecode-reviewadrengineering-practice
Architecture reviews

def f(...) -> T: """What it does, what it raises, why (not obvious from code)."""

A docstring earns its keep by stating a contract the signature does not already make obvious; a README gets a new engineer running.

function_doc("charge_card", ["customer_id: str"], "a Charge record", ["CardDeclinedError"])

documentationreadabilityengineering-practice
Technical documentation

log.append({"correlation_id": correlation_id, "step": "...", ...})

Thread a correlation ID through every downstream call so a production request's full log timeline can be reconstructed after the fact.

isolated = [e for e in log if e["correlation_id"] == failing_cid]

debuggingproductionobservabilityengineering-practice
Debugging production incidents

(optimistic + 4 * most_likely + pessimistic) / 6

PERT three-point estimation weights the most-likely case heavily while still accounting for best and worst case.

three_point_estimate(3, 8, 21) # -> 9.3

estimationplanningengineering-practice
Estimating engineering work

task.get("behind_flag") or task.get("additive")

A subtask is safe to ship on its own if it is additive or hidden behind a feature flag — save the risky cutover for last.

shippable = [t for t in subtasks if is_independently_shippable(t)]

planningfeature-flagsengineering-practice
Breaking large tasks into smaller pieces

re.search(r"#\s*(TODO|FIXME|HACK|XXX)", line)

A debt-marker scan finds tracked, commented shortcuts — a starting point, not the whole picture of a codebase's technical debt.

find_debt_markers(source_lines) # -> [(2, "# HACK: ..."), (6, "# TODO: ...")]

technical-debtcode-qualityengineering-practice
Identifying technical debt

observed = legacy_func(*args); assert observed == <actual current output>

A characterization test pins down what legacy code currently does (bugs included) so a later refactor has something real to check against.

assert legacy_discount(19.99, 10) == 179.9 # documents real behavior, not "correct" behavior

refactoringlegacy-codetestingengineering-practice
Refactoring legacy systems

"new_service" if path in migrated_paths else "legacy_service"

A strangler-fig router sends already-migrated paths to the new system and leaves everything else on the legacy one.

strangler_route("/api/v2/invoices") # -> "new_service"

migrationarchitectureengineering-practice
Migration strategies

class MigrationNNNN: revision = "N" down_revision = "N-1" def upgrade(): ... def downgrade(): ...

A versioned schema migration chains to the prior revision and pairs every upgrade() with a genuine downgrade() inverse.

apply_migration(Migration0007AddLastLogin, "downgrade")

migrationdatabaseschemaengineering-practice
Database migrations

next_canary_step(percent, errors, total) -> new_percent

Widen canary traffic gradually while error rate stays under threshold; any breach reverts to 0% traffic immediately.

next_canary_step(50, errors=40, total=500) # -> 0 (breach)

deploymentcanaryreliabilityengineering-practice
Safe deployments

review_comment(kind, text, teach=None)

A review comment that states the underlying "why" alongside a correction teaches a reusable lesson, not just a one-line fix.

review_comment("blocking", "mutable default", teach="evaluated once at def time")

mentoringcode-reviewengineering-practice
Mentoring junior developers

compare_options([{name, gain, cost, recommended}, ...])

Present each option as gain versus cost in plain language, and mark an explicit recommendation rather than leaving the decision open.

compare_options(options) # "-> Add Redis cache: gain=...; cost=..."

communicationtrade-offsengineering-practice
Communicating technical trade-offs

What a 5-Year Python Engineer Should Be Able to Explain — Python

1

def f(x): ... # x is bound to the argument object, like x = argument

Python has one binding rule, not two calling conventions: a parameter name becomes another name for the argument object. Reassigning the parameter never reaches the caller; mutating the object in place does.

def f(x): x.append(1) # caller sees this x = [] # caller does not see this

functionsmutabilitynames-and-referencesinterview
How Python passes objects to functions

What a 5-Year Python Engineer Should Be Able to Explain — Concurrency

1

choose_model(cpu_bound, concurrent_operations) -> "multiprocessing" | "asyncio" | "threading"

CPU-bound work always needs multiprocessing regardless of concurrency scale; I/O-bound work needs asyncio only once concurrency scale is large, otherwise threading is simpler.

choose_model(cpu_bound=False, concurrent_operations=5000) # -> "asyncio"

concurrencyasynciothreadingmultiprocessinginterview
Choosing a concurrency model under real constraints

What a 5-Year Python Engineer Should Be Able to Explain — Backend

1

reverse proxy -> WSGI/ASGI server -> worker -> framework routing -> view function

A request crosses four distinct hops before application code runs — naming all four, not just "nginx forwards to my app," is the complete answer.

HOPS = ["reverse proxy", "WSGI/ASGI server", "worker", "framework view"]

What a 5-Year Python Engineer Should Be Able to Explain — Database

6

index = sorted(column) -> row pointer · log2(n) seek · one write per index per row change

An index trades write cost for read speed, and only pays off on a selective predicate. A condition matching 95% of the table is answered faster by scanning.

100,000 rows: full scan 99,999 comparisons, index lookup 17

databaseindexperformancepostgresqlinterview
What an index is, and when it hurts

atomic = all or nothing · isolation = what you see of concurrent transactions

Atomicity protects you from your own crash; isolation protects you from other transactions. A read-modify-write loses updates under Read Committed — use a relative or conditional update.

Both read 100, one writes 70, the other writes 50 -> the first withdrawal is gone

databasetransactionsisolationpostgresqlinterview
What a transaction is, and what isolation means

has_deadlock_cycle({tx_id: blocked_on_tx_id}) -> bool

A deadlock is a wait-for cycle between transactions, almost always from opposite-order row locking — fixed by always locking shared rows in a consistent order.

has_deadlock_cycle({"A": "B", "B": "A"}) # -> True

databasedeadlocktransactionspostgresqlinterview
What causes database deadlocks

1. index? 2. N+1 (check query COUNT)? 3. SELECT */no LIMIT? 4. sargable WHERE? 5. EXPLAIN ANALYZE

A fixed diagnostic order for a slow query — index existence and hidden N+1 patterns explain most cases before a query plan is ever needed.

len(queries_executed) # 6 (N+1) vs 1 (JOIN) for the same 5 orders

databaseperformancen-plus-1sqlinterview
How to optimize a slow query

document = assemble at write time · relational = assemble at read time

Pick by access patterns: one aggregate read whole favours documents, many cross-cutting questions favour relational. Embedding duplicates values, and every copy must be updated.

One order: 1 read vs 2. Revenue by product: 800 items touched vs 600. Rename: 200 copies vs 1 row.

databasesqlnosqldata-modelinginterview
SQL vs NoSQL

should_use_redis(needs_durability, needs_relational_queries) -> bool

Redis fits caching, ephemeral data, and its native structures — not durable systems of record or relational queries.

should_use_redis(needs_durability=True, needs_relational_queries=False) # -> False

rediscachingarchitectureinterview
When would you use Redis?

What a 5-Year Python Engineer Should Be Able to Explain — Architecture

1

store.append(f"cart:{user_id}", item) # external store, not a module-level dict

A horizontally scalable Python service externalizes every piece of per-user state into a shared store every worker process can reach.

shared_store.append("cart:user-1", "widget") # any worker, any machine, sees this

scalabilityarchitecturestatelessnessinterview
What makes a Python service horizontally scalable