Matheion

MA-PY1 · Module 4.6

Decorators and context managers

Topics: decorator · functools.wraps · with · __enter__ · __exit__

Overview

Module 4.6 — Decorators and context managers

Scroll Core ideas → worked snippets → Practice (quiz). Attempt each question before Discuss / reveal. Use any Python REPL, script, or notebook you like.

Concepts

Core ideas — decorators & context managers

Decorators (callables transforming callables)

A decorator D takes a function f and returns a new callable f'—usually wrapping f to add timing, logging, validation, retries, etc.

def loud(f):
    def wrapper(*args, **kwargs):
        print(f"calling {f.__name__}")
        return f(*args, **kwargs)

    return wrapper


@loud  # countdown = loud(countdown)
def countdown(n):
    return list(range(n, -1, -1))

Writing **@D directly above def is equivalent to assigning fn = D(fn) after def finishes.

Stacked decorators apply bottom-up:

@a
@b
def f(): ...

means f = a(b(f)) (the decorator **immediately above def applies first: here b wraps f first, then a wraps that result).

Preserve help() names/docs on the wrapper using functools.wraps:

import functools

def traced(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        print("enter")
        try:
            return f(*args, **kwargs)
        finally:
            print("leave")

    return wrapper

Parameterized decorators (“decorator factories”) accept configuration and return the real decorator—you see two nested def layers (retry(n) → deco(f) → wrapper).

Standard-library patterns you’ll read often: @classmethod, @staticmethod, @property (descriptor-based, decorator-shaped). Earlier, with classes, these were deferred; now you can see why they exist — they are ordinary decorators that adjust how a method is called. @staticmethod drops the automatic self; @classmethod swaps self for the class itself (handy for alternative constructors); @property lets a method be read like a plain attribute. You do not need to write your own yet, but you will now recognise the shape when you read them.

Context managers (with)

with guarantees cleanup runs when leaving the block—even on exceptions—when the object implements __enter__ and __exit__ (or you use contextlib.contextmanager with a yield generator).

with open(path, encoding="utf-8") as f:
    data = f.read()
# file closed here

path in snippets should be replaced with a real string or Path in your sandbox.

Contrast: decorators shape callable objects for reuse; context managers orchestrate lifetimes. They often appear together in frameworks.

Self-study checklist

  • Confirm stacking order @a then @b with print stubs printing which wrapper runs outermost first call.
  • Read **functools.wraps documentation once—note which metadata propagate.
  • List three chores with excels at (files, locks, timing sections with contextlib, DB transactions…).

Examples

Worked snippets — decorators & with

Basic decorator

import functools


def starred(f):
    @functools.wraps(f)
    def wrapper(name):
        return "-- " + f(name) + " --"

    return wrapper


@starred
def salute(name):
    return f"hi {name}"


print(salute.__name__)
print(salute("Ada"))

Timing wrapper (minimal)

import functools
import time


def timed(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        try:
            return f(*args, **kwargs)
        finally:
            print(f"{f.__name__}: {time.perf_counter() - t0:.6f}s")

    return wrapper


@timed
def slow_sum(n):
    return sum(range(n))

print(slow_sum(10_000))

Decorator stacking order

import functools


def annotate(tag):
    def deco(f):
        @functools.wraps(f)
        def wrapper():
            print(tag, end=" ")
            return f()

        return wrapper

    return deco


@annotate("A")
@annotate("B")
def speak():
    print("speech")


speak()  # outermost annotate runs immediately around inner wrappers

(Expect A B before speech with this construction—still verify locally.)

classmethod refresher

class Thing:
    kind = "widget"

    @classmethod
    def label(cls):
        return cls.kind


print(Thing.label())

Simple context manager via class

class Indent:
    def __init__(self, txt):
        self.txt = txt

    def __enter__(self):
        print(self.txt + " start")
        return self

    def __exit__(self, exc_type, exc, tb):
        print(self.txt + " end")
        return False


with Indent("phase"):
    print("  body")

with invokes __enter__, runs the body indented block, then __exit__ even if body raises.

Practice check — decorators and context managers

Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.

  1. 1.Writing `@deco` directly above `def f(): ...` is equivalent to which statement after the definition?

  2. 2.Given stacked decorators, what is the effective call order? ```python @a @b def f(): ... ```

  3. 3.Why wrap a decorator's inner function with `@functools.wraps(f)`?

  4. 4.What does a `with` block guarantee for a proper context manager, even if an exception occurs inside it?

  5. 5.Which pair of methods must an object implement to be usable as a context manager in a `with` statement?