Function 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) 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. When writing decorators, you must be careful because the wrapper replaces the original function entirely. This causes the function to lose its original metadata, such as its name and docstring. def simple_decorator(func): def wrapper(): return func() return wrapper @simple_decorator def greet(): pass print(greet.__name__) # Prints 'wrapper', not 'greet' This metadata loss can break debugging tools or web frameworks that rely on function names. This trap is fixed using `@wraps`, which is covered fully in functools-wraps.
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?
- What happens to a function's metadata (like `__name__`) when it is wrapped by a simple decorator?
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
Add Logging to a Decorator
The current `log_call` decorator doesn't do anything yet. Modify it so that it prints `'Calling function'` before executing the decorated function, and `'Function finished'` after. Apply it to the `say_hello` function.
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: Defining functions and return values
Previous: First-class functions: passing and returning functions
Previous: Closures
Next: functools.wraps and preserving metadata