Python Decorators — Explained with Examples
A Python decorator is a function that takes another function and extends its behavior without modifying the original code. Decorators use the @ syntax and are a powerful form of metaprogramming.
What You’ll Learn
- How functions are first-class objects in Python
- The
@syntax and how decorators work under the hood functools.wrapsto preserve metadata- Decorators with arguments, class decorators, and built-in decorators
- Real-world example: building a timing decorator
Why Decorators Matter
Durga Antivirus Pro uses decorators to log every scan operation, measure scan duration, and cache results. DodaZIP decorates compression functions to add progress bars and error logging. Without decorators, you’d repeat boilerplate code in every function — logging, timing, access control, caching. Decorators let you write that logic once and apply it anywhere.
flowchart LR
A["OOP"] --> B["Decorators"]
B --> C["Generators"]
C --> D["Context Managers"]
D --> E["Async"]
A:::done --> B:::current
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#2563eb,stroke:#2563eb,color:#fff
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Functions Are First-Class Objects
In Python, functions are objects — you can assign them to variables, pass them as arguments, and return them:
def greet(name: str) -> str:
return f"Hello, {name}!"
say_hello = greet # Assign to variable
print(say_hello("Alice")) # Hello, Alice!
def call_twice(func, arg):
return func(arg), func(arg)
print(call_twice(greet, "Bob")) # ('Hello, Bob!', 'Hello, Bob!')This first-class nature is what makes decorators possible.
How a Decorator Works
A decorator is just a function that wraps another function:
# Manual decoration (without @)
def make_uppercase(func):
def wrapper(text: str) -> str:
result = func(text)
return result.upper()
return wrapper
def greet(name: str) -> str:
return f"Hello, {name}"
greet = make_uppercase(greet)
print(greet("Alice")) # HELLO, ALICE!The @ syntax is sugar for greet = make_uppercase(greet):
def make_uppercase(func):
def wrapper(text: str) -> str:
result = func(text)
return result.upper()
return wrapper
@make_uppercase
def greet(name: str) -> str:
return f"Hello, {name}"
print(greet("Alice")) # HELLO, ALICE!Preserving Metadata with functools.wraps
Without wraps, the decorated function loses its name and docstring:
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def say_hello():
"""Says hello."""
pass
print(say_hello.__name__) # wrapper (not say_hello!)
print(say_hello.__doc__) # NoneFix with @functools.wraps:
import functools
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def say_hello():
"""Says hello."""
pass
print(say_hello.__name__) # say_hello
print(say_hello.__doc__) # Says hello.Always use @functools.wraps in your decorators.
Real-World Example: Timing Decorator
import functools
import time
def timer(func):
"""Print how long a function takes to run."""
@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
@timer
def slow_function():
time.sleep(0.5)
return "Done"
print(slow_function())
# slow_function took 0.5002s
# DoneDecorators with Arguments
Sometimes you want to pass arguments to your decorator. Add an extra layer:
import functools
def repeat(times: int = 2):
"""Run the decorated function `times` times."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name: str) -> str:
print(f"Hello, {name}!")
return f"Hello, {name}!"
greet("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!Without () — @repeat without arguments — the decorator is applied with default times=2.
Built-in Decorators
@staticmethod and @classmethod
class Calculator:
@staticmethod
def add(a: int, b: int) -> int:
return a + b
@classmethod
def from_string(cls, expr: str):
a, op, b = expr.split()
if op == "+":
return cls.add(int(a), int(b))
raise ValueError(f"Unknown operator: {op}")
print(Calculator.add(3, 4)) # 7
print(Calculator.from_string("3 + 4")) # 7@property
Already covered in the Python https://tutorials.dodatech.com/programming-languages/python/py-oop/ tutorial — turns a method into a read-only attribute with optional setter.
Class Decorators
You can decorate entire classes too:
def add_repr(cls):
"""Add a __repr__ method to a class."""
def __repr__(self) -> str:
items = ", ".join(f"{k}={v}" for k, v in self.__dict__.items())
return f"{cls.__name__}({items})"
cls.__repr__ = __repr__
return cls
@add_repr
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
p = Point(3, 4)
print(p) # Point(x=3, y=4)Common Mistakes
1. Forgetting to Call functools.wraps
Without it, debugging becomes painful — all decorated functions appear as wrapper.
2. Forgetting the Inner Function in Parametrized Decorators
def decorator(arg): # This runs at decoration time
# Missing inner decorator function!
def wrapper(*args, **kwargs):
pass
return wrapperFix: Always have three levels: outer(arg) -> decorator(func) -> wrapper(*args, **kwargs).
3. Applying a Non-Callable as a Decorator
@42 # TypeError: 'int' object is not callable
def f():
pass4. Modifying Mutable Arguments Across Calls
If your decorator stores state in a mutable default (like []), all decorated functions share the same state.
5. Wrapping a Class Method Without @functools.wraps
This breaks self binding. Always wrap and preserve the signature.
Practice Questions
1. What does @timer above do when applied to a function?
It prints how long the function took and returns the original result.
2. Why does wrapper.__name__ become "wrapper" without @functools.wraps?
Because wrapper is the actual function object returned by the decorator. wraps copies __name__, __doc__, etc. from the original.
3. Fix this decorator:
def log(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper # OK, but missing @functools.wraps4. Write a decorator that returns "Access Denied" if the first argument is not "admin".
def require_admin(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if user != "admin":
return "Access Denied"
return func(user, *args, **kwargs)
return wrapperChallenge: Build a retry(max_attempts=3) decorator that retries a function if it raises an exception.
Solution
import functools
import time
def retry(max_attempts: int = 3, delay: float = 0.5):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed, retrying...")
time.sleep(delay)
return None
return wrapper
return decoratorMini Project: Logging and Timing Decorators
import functools
import time
import logging
logging.basicConfig(level=logging.INFO)
def log_and_time(func):
"""Log function calls and measure execution time."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = ", ".join(repr(a) for a in args)
kwargs_repr = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
all_args = ", ".join(filter(None, [args_repr, kwargs_repr]))
logging.info(f"Calling {func.__name__}({all_args})")
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
logging.info(f"{func.__name__} returned {result!r} in {elapsed:.4f}s")
return result
return wrapper
@log_and_time
def compute_square(n: int) -> int:
return n * n
print(compute_square(42))
# INFO:root:Calling compute_square(42)
# INFO:root:compute_square returned 1764 in 0.0000s
# 1764Expected output: Log lines followed by the result.
What’s Next
Decorators open the door to advanced Python patterns. Next, learn generators for lazy evaluation and context managers for resource cleanup.
| Topic | Description | Link |
|---|---|---|
| Python Generators | Yield and lazy iteration | https://tutorials.dodatech.com/programming-languages/python/py-generators/ |
| Python Context Managers | With-statement patterns | https://tutorials.dodatech.com/programming-languages/python/py-context-managers/ |
| OOP glossary | OOP terminology reference | OOP |
Practice tip: Combine decorators with your BankAccount class from the OOP tutorial — add @log_and_time to every method. Notice how decorators let you add cross-cutting concerns without touching existing code.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro