Decorators and type hints
Organize a small program into modules and classes while handling files, failures and iteration.
THE IDEA
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)
Printed
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.
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 * 2What 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'>}.
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.
TRY IT YOURSELF
Exercise 1: write a decorator
- Write
count_calls(func). It returns a wrapper that counts how many times the function was called inwrapper.calls, passes all arguments (positional and keyword) through, and returns the real result. - Use
@functools.wraps(func)on the wrapper so the decorated function keeps its__name__and docstring. - Each decorated function needs its own counter, and an exception raised by the function must still propagate (the call is still counted).
Exercise 2: make hints enforce something
- Type hints alone enforce nothing at run time. Write a decorator
enforce_types(func)that adds a check: before calling the function it looks at each **annotated** argument and raisesTypeErrorif the value is not an instance of the annotation. - The message must name the parameter, for example
x must be int, got str. Parameters without an annotation are not checked, and neither are default values you did not pass. The return annotation is ignored. - Hint:
inspect.signature(func).bind(*args, **kwargs).argumentsmaps parameter names to the values that were passed. An unannotated parameter has the annotationinspect.Parameter.empty(itself a class!), so skip that one explicitly, then check only annotations that are classes.
Both exercises must be solved to finish this lesson.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Does x: int = 'cat' automatically raise a Python type error?
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.