Ads and ad measurement. Disabled until you allow it.
ABOUT SITE
All open site tabs
Python · Syntax · Advanced
Decorators
10 tasks
Functions that wrap other functions to modify or extend their behaviour. Covers `@` syntax, decorators with arguments, stacking decorators, and `functools.wraps`.
Quick topic start and explanations before exercises (exercises below):
Why decorators exist, closures, anatomy, and functools.wraps
**Why decorators exist**
Imagine you want to log every time any function is called, or measure how long each function takes. Without decorators you'd copy-paste timing code into every function. Decorators let you write that logic once and attach it anywhere:
```python
# Without decorators — you'd repeat this everywhere:
def compute(n):
start = time.perf_counter()
result = sum(range(n))
print(f'took {time.perf_counter() - start:.4f}s')
return result
# With a decorator — timing logic lives in one place:
@timer
def compute(n):
return sum(range(n))
```
This is called a *cross-cutting concern* — behaviour that belongs to many functions but isn't part of any single one's job. Decorators are Python's answer to that.
**What `@decorator` actually does**
The `@` syntax is pure shorthand. These two are identical:
```python
@timer
def greet(name):
return f'Hello, {name}!'
# Same as:
def greet(name):
return f'Hello, {name}!'
greet = timer(greet) # greet is now the wrapper timer returned
```
Python evaluates the decorator first, passes the function to it, and rebinds the original name to whatever the decorator returns.
**Why closures make this work**
The `wrapper` function inside a decorator *closes over* the `func` parameter — it remembers a reference to the original function even after `my_decorator` has returned. This is the fundamental mechanism:
```python
def my_decorator(func): # func is 'greet' at decoration time
def wrapper(*args, **kwargs):
print('before') # wrapper closes over func
result = func(*args, **kwargs) # still has access to 'func'
print('after')
return result
return wrapper # return the closure, not the result
```
The `*args, **kwargs` in the wrapper signature means the decorator works with *any* function, regardless of how many parameters it takes.
**Always use `@functools.wraps`**
Without it, the wrapper replaces the original function's identity. Debugging becomes confusing, and frameworks that inspect `__name__` or `__doc__` (like FastAPI route discovery or pytest) will behave unexpectedly:
```python
import functools
def my_decorator(func):
@functools.wraps(func) # copies __name__, __doc__, __annotations__
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name: str) -> str:
'Say hello to name.'
return f'Hello, {name}!'
greet.__name__ # 'greet' (not 'wrapper')
greet.__doc__ # 'Say hello to name.'
greet.__annotations__ # {'name': str, 'return': str}
# Without @wraps:
# greet.__name__ would be 'wrapper' — hard to debug
```
`functools.update_wrapper(wrapper, func)` is the explicit version of the same thing — `@wraps(func)` is just shorthand for calling it.
**Decorator with arguments — the factory pattern**
A plain decorator takes a function and returns a function. If you need to pass *parameters* to the decorator itself, you add one more layer — a factory that accepts the parameters and returns the actual decorator:
```python
import functools
def repeat(n): # layer 1: factory — accepts the argument
def decorator(func): # layer 2: actual decorator
@functools.wraps(func)
def wrapper(*args, **kwargs): # layer 3: wrapper
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3) # repeat(3) returns the decorator; that decorator wraps say
def say(msg):
print(msg)
say('hello') # prints 'hello' three times
```
The pattern: each `@decorator(arg)` call returns a decorator, and that decorator is what wraps the function.
**Stacking decorators**
You can apply multiple decorators to one function. They're applied bottom-up at decoration time, but called top-down at runtime:
```python
@timer # applied second → outermost layer
@log_calls # applied first → innermost layer
def compute(n):
return sum(range(n))
# Equivalent to:
compute = timer(log_calls(compute))
# Call order at runtime:
# timer's wrapper runs → log_calls' wrapper runs → compute runs
```
A common use of stacking: `@functools.lru_cache` combined with a custom decorator that clears the cache on certain conditions.
**Class-based decorator**
Any object with a `__call__` method can act as a decorator. This is useful when the decorator needs to maintain state across calls:
```python
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func) # same as @wraps but for classes
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountCalls
def say_hi():
print('hi')
say_hi()
say_hi()
print(say_hi.count) # 2 — state lives on the CountCalls instance
```
Class-based decorators are the right choice when you need to expose extra methods or attributes (like `.count` above) alongside the callable.
> **Note on class-method decorators:** Python also ships `@property`, `@classmethod`, and `@staticmethod` — built-in decorators that change how a method binds to its class. These are a distinct topic covered in depth in the next section.
Real patterns, common mistakes, when not to use, method decorators preview
**Common real-world decorator patterns**
```python
import functools, time
# 1. Timer — measure execution time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f'{func.__name__} took {elapsed:.4f}s')
return result
return wrapper
# 2. Retry on failure
def retry(n=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, n + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == n:
raise
return wrapper
return decorator
# 3. Singleton — ensure only one instance is created
def singleton(cls):
instances = {}
@functools.wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Config:
def __init__(self):
self.debug = False
Config() is Config() # True — same instance every time
```
**Common mistakes**
**1. Forgetting to return the result:**
```python
def bad_decorator(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs) # BUG: result is silently discarded
return wrapper
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) # always return
return wrapper
```
If the original function returns a value and the wrapper doesn't return it, callers silently get `None` — often hard to notice until tests fail.
**2. Returning the wrapper call instead of the wrapper:**
```python
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper() # BUG: calls wrapper immediately, returns result
def good_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # correct: return the function object, not a call
```
**3. Forgetting `@functools.wraps`:**
```python
help(greet) # shows 'wrapper' instead of 'greet' docs
greet.__name__ # 'wrapper' — breaks introspection-based tools
```
**When NOT to use a decorator**
Decorators add a call indirection and hide the original function's logic. Prefer a decorator when:
- The behaviour applies to many unrelated functions
- It's purely additive (logging, timing, caching, validation)
- It doesn't change the function's signature or return type
Avoid decorators when the behaviour is specific to one function, when it needs to change the return type, or when it makes the code significantly harder to follow — in those cases a plain helper function or a base class is usually clearer.
**Preview: built-in method decorators**
Python ships `@property`, `@classmethod`, and `@staticmethod` as built-ins. These decorate methods on a class rather than standalone functions, and they interact with Python's descriptor protocol — a separate mechanism from the function-decorator pattern described in this section. They are covered in full in the next topic.
Write a decorator `timer` that measures how long the decorated function takes to run and prints the result in the format: `'greet took 0.0001s'`. Use `time.time()` for measurement.
import time
def timer(func):
pass
@timer
def greet(name):
return f'Hello, {name}!'
greet('Alice') # prints: greet took 0.0001s (approx)
Solution
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f'{func.__name__} took {elapsed:.4f}s')
return result
return wrapper
@timer
def greet(name):
return f'Hello, {name}!'
greet('Alice')
Write a decorator `log_calls` that prints the function name, its positional arguments, and return value each time it is called. Format: `'add called with (2, 3) -> 5'`.
def log_calls(func):
pass
@log_calls
def add(a, b):
return a + b
add(2, 3) # prints: add called with (2, 3) -> 5
add(10, 20) # prints: add called with (10, 20) -> 30
Solution
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f'{func.__name__} called with {args} -> {result}')
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3)
add(10, 20)
Write a decorator `memoize` that caches function results. On repeated calls with the same arguments, return the cached value without calling the function again. Store results in a dict keyed by `args`.
def memoize(func):
pass
@memoize
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(10)) # 55
print(fib(30)) # 832040 (fast with cache)
Solution
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(10)) # 55
print(fib(30)) # 832040
Write a decorator `count_calls` that tracks how many times the decorated function has been called. Store the count as a `.count` attribute on the wrapper so callers can read it.
Write a decorator `validate_positive` that checks every positional argument before calling the function. If any argument is not a positive number (≤ 0), raise `ValueError` with the message `'All arguments must be positive'`.
def validate_positive(func):
pass
@validate_positive
def area(width, height):
return width * height
print(area(3, 4)) # 12
print(area(-1, 5)) # ValueError: All arguments must be positive
Solution
import functools
def validate_positive(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if any(a <= 0 for a in args):
raise ValueError('All arguments must be positive')
return func(*args, **kwargs)
return wrapper
@validate_positive
def area(width, height):
return width * height
print(area(3, 4)) # 12
try:
area(-1, 5)
except ValueError as e:
print(e) # All arguments must be positive
Write a decorator `once` that ensures the decorated function executes only once. On the first call, run the function and store its result. On all subsequent calls, return the stored result without executing again.
Write a decorator factory `retry(n)` that retries the decorated function up to `n` times if it raises any exception. If all attempts fail, re-raise the last exception. Print `'Attempt X failed'` on each failure before retrying.
Create a `Temperature` class that stores temperature in Celsius internally. Use `@property` to expose a `celsius` property (getter and setter) and a `fahrenheit` property that also has a setter which converts from Fahrenheit to Celsius. Formula: `F = C * 9/5 + 32`.
Write a decorator factory `requires_role(role)` that protects a function. The decorated function always receives a `user` dict as its first argument. If `user['role']` does not equal `role`, raise `PermissionError` with `'Access denied'`. Otherwise, call the function normally.
def requires_role(role):
pass
@requires_role('admin')
def delete_record(user, record_id):
return f"Record {record_id} deleted by {user['name']}"
admin = {'name': 'Alice', 'role': 'admin'}
guest = {'name': 'Bob', 'role': 'guest'}
print(delete_record(admin, 42)) # Record 42 deleted by Alice
delete_record(guest, 42) # PermissionError: Access denied
Stack the `timer` and `log_calls` decorators on the same function `compute(n)` that returns the sum of squares from 1 to n. Apply `@timer` on the outside and `@log_calls` on the inside. Verify the output shows both the log line and the timing line.
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f'{func.__name__} took {time.time() - start:.4f}s')
return result
return wrapper
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f'{func.__name__} called with {args} -> {result}')
return result
return wrapper
# Stack both decorators on compute:
def compute(n):
return sum(i * i for i in range(1, n + 1))
compute(5)
Solution
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f'{func.__name__} took {time.time() - start:.4f}s')
return result
return wrapper
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f'{func.__name__} called with {args} -> {result}')
return result
return wrapper
@timer
@log_calls
def compute(n):
return sum(i * i for i in range(1, n + 1))
compute(5)
# log_calls prints: compute called with (5,) -> 55
# timer prints: compute took 0.0000s
No split tab
Cookie preferences
We use necessary cookies to run the site. With your permission, we can also save your site preferences and use analytics and advertising cookies to understand usage and support the project.
Open tools in tabs.Exercises, IDE tools, and trainers stay available as site tabs.
Switch without losing context.Move between explanations, code, and utilities while keeping your place.
Use the sidebar as your map.The left panels hold navigation, settings, files, libraries, and tool controls.
PythonJavaScriptSQLite
One IDE, three practical modes
Python in the browser.Run small scripts, try libraries, and practice API requests without installing anything.
JavaScript for quick experiments.Test browser-friendly code and compare ideas next to your learning materials.
SQLite for data practice.Open the database explorer to inspect tables, write queries, and learn SQL workflows locally.
TopicIDE
Work side by side with split tabs
Keep instructions visible.Open an exercise or reference page beside the IDE instead of jumping back and forth.
Compare tools while you learn.Place regex checks, explanations, and code experiments next to each other when the task needs it.
Close the split when you are done.The workspace returns to a single focused tab, and your open site tabs remain available.
Code Typing Trainer
Or plain text
This trainer is designed for a physical keyboard.Open this section on a laptop or desktop with a wide screen. Touch typing practice will not work correctly on a phone.
Speed: 0 chars/min
0 words/min
Best speed (60s): 0 chars/min
0 words/min
Errors: 0
Total time: 0.0 s
To practice touch typing, avoid looking at your physical keyboard.