Foundations: Python, Git and toolsLoading progress…

Decorators and type hints

Organize a small program into modules and classes while handling files, failures and iteration.

Functions are values, so you can wrap them

In Python a function is an object like any other: you can pass it to another function and return a new function. A decorator uses that. It takes a function and returns a replacement that usually does something before and after calling the original. The line @logged above a definition is shorthand for add = logged(add).

def logged(func): def wrapper(*args, **kwargs): print("calling", func.__name__) result = func(*args, **kwargs) print("returned", result) return result return wrapper @loggeddef add(a, b): """Add two numbers.""" return a + b add(2, 3)

Call stack (top = running now)

module

Printed

step 1 of 7

Decoration time. @logged runs logged(add), which builds a wrapper function that remembers add. The name add is then bound to the WRAPPER, not to the original.

Step through the call add(2, 3). Watch which function owns each stack frame, then switch to functools.wraps and look at the last step.

The wrapper takes *args, **kwargs so it can accept whatever the original accepts and pass it all on, and it must return the original's result. Forgetting either is the usual first bug.

Why functools.wraps matters

Without help, the name add now points at wrapper, so add.__name__ says "wrapper" and the docstring is gone. That breaks debugging output, documentation and tools that look at function names. @functools.wraps(func) on the wrapper copies the name, docstring and other metadata, and sets __wrapped__ to the original.

Predict first. A decorator does not use functools.wraps. What is add.__name__ for the decorated add?

Type hints: documentation that tools read

A type hint such as x: int or -> int states what you intend. Editors and static checkers (mypy, pyright) read hints to find mistakes before you run anything. Python itself only stores them: they are not checked when the function is called.

def double(x: int) -> int: return x * 2

What Python does when it runs

8

No check against the hint happened. The hint was never consulted.

What a static checker says (mypy, pyright)

fine

A static checker reads your code without running it and compares calls to the hints.

At run time the hints are only stored: double.__annotations__ is {'x': <class 'int'>, 'return': <class 'int'>}.

Choose a call. The runtime column shows what Python really does; the right column is what a static checker would say about the same line.

Predict first. Does x: int = "cat" raise an error when it runs?

Chapter context

As a program grows, the difficult part becomes keeping state and responsibilities understandable. Use simple functions for transformations, classes for related state and behavior, and modules to give those responsibilities names.

Chapter reference: math, worked example & code

This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.

Math and notation

Reason about state ownership before adding abstraction. For a transformation f, repeated calls should have independent results unless shared state is explicitly intended. A lazy iterator retains only the state required for its next step; it does not imply that downstream consumers also use constant memory.

Worked example

An inventory starts with 5 items. reserve(2) leaves 3. A second independent inventory still contains 5. If both objects instead refer to the same list, mutating one changes what both observe. Draw object references as arrows to make this distinction explicit.

Reference code

The following example uses the Python standard library and does not call a remote service.

class Inventory:
    def __init__(self, count):
        self.count = count
    def reserve(self, n):
        if n > self.count:
            raise ValueError("insufficient stock")
        self.count -= n
first, second = Inventory(5), Inventory(5)
first.reserve(2)
assert (first.count, second.count) == (3, 5)

Python examples are provided for study and local execution; this page does not execute Python.

Common mistakes

Type annotations are not validation. Catching Exception and continuing can hide corrupt state. Lazy iteration does not make a later list(...) memory-free.