Decorators
Overview
A decorator is a function that takes another function and extends its behavior without explicitly modifying it. The `@decorator` syntax is syntactic sugar for `func = decorator(func)`. Under the hood, a decorator returns a wrapper function (a closure, which is a function that retains access to its enclosing scope) that replaces the original function. It acts like gift wrapping a box: the original box is still inside, but it now has a shiny exterior.
Decorators allow you to cleanly extract reusable cross-cutting concerns—like logging, authentication, or timing—away from the core business logic of your functions.
Where used: FastAPI route definitions (@app.get), Flask routes (@app.route), Built-in class properties (@property)
Why learn this
- You can wrap dozens of functions with repetitive logic like access control with just one line of code.
- It is the foundational mechanism for web framework routing in Python.
Code walkthrough
def uppercase(func):
def wrapper():
original_result = func()
return original_result.upper()
return wrapper
@uppercase
def greet():
return 'hello'
print(greet())
Focus: return original_result.upper()
Aha moment
def no_op(func):
pass
@no_op
def greet():
return 'hello'
print(greet())
Prediction: What does this code print?
Common guess: It prints 'hello', assuming the decorator does nothing.
It throws a TypeError. Since `no_op` doesn't explicitly return anything, it returns `None`. The decorator syntax does `greet = no_op(greet)`, so `greet` is now `None`.
Common mistakes
- Forgetting to return the wrapper: Without this `return` statement, the decorator evaluates to `None`, silently breaking the function it wraps.
- Forgetting to return the original function's result: The inner wrapper must pass back the result of `func(*args, **kwargs)` so the caller receives the expected value.
Glossary
- syntactic sugar
- Syntax in a programming language that makes things easier to read or to express, but doesn't add any new functionality. For example, `@my_decorator` is syntactic sugar for `func = my_decorator(func)`. Example: `@timer`
- cross-cutting concerns
- Aspects of a program that affect many different parts of the system, like logging or security, rather than just one specific feature. Example: `@log_calls`
Recall questions
- What is the equivalent manual code for `@my_decorator\ndef my_func(): pass`?
- What kind of object does a decorator function typically return?
- Why use decorators instead of just writing the logic inside the function?
Understanding checks
What will this code print?
<b>Hello</b>
The decorator `make_bold` wraps `get_text`. When `get_text()` is called, it actually calls `wrapper()`, which evaluates `func()` (returning 'Hello'), wraps it in `<b>` tags, and returns the result.
This decorator is meant to log the execution of a function, but calling `add(2, 3)` returns `None`. Why?
The `wrapper` function forgot to return the result of `func(*args, **kwargs)`.
In a decorator, the `wrapper` replaces the original function. If the original function calculates a value but the `wrapper` doesn't explicitly return it, the caller will implicitly receive `None`.
Practice tasks
Challenge
Write a timing decorator that accepts arguments
Create a decorator `timer` that measures the execution time of a function. It must accept any number of positional and keyword arguments (`*args`, `**kwargs`), run the original function, print the time taken in seconds (you can mock the time or use `import time`), and return the original function's result.
Continue learning
Previous: Closures & LEGB scope
Next: functools.wraps