Introduction
Decorators are one of Python’s most powerful—and often confusing—features. At first glance, the @decorator syntax can feel like magic. But once you understand how decorators work, they become an elegant tool for writing cleaner, more reusable code.
Table of contents
- Introduction
- What Is a Decorator in Python?
- A Simple Function Example
- Writing Your First Decorator**
- The @decorator Syntax
- Decorators with Arguments
- Preserving Function Metadata (functools.wraps)
- Decorators with Arguments (Advanced)
- Common Real-World Use Cases
- Built-in Decorators You Already Use
- When Should You Use Decorators?
- References
What Is a Decorator in Python?
A decorator is a function that:
- Takes another function as input
- Extends or modifies its behavior
- Returns a new function
Key idea: decorators let you add functionality without changing the original function’s code.
In Python, functions are first-class objects, meaning:
- They can be passed as arguments
- They can be returned from other functions
- They can be assigned to variables
Decorators are built on top of this concept.
A Simple Function Example
Let’s start with a basic function:
1 2 | def greet(): print("Hello!") |
Calling it:
1 | greet() |
Output:
1 | Hello! |
Now suppose we want to:
- Print something before and after
greet()runs
We could modify the function—but that doesn’t scale well.
This is where decorators shine.
Writing Your First Decorator**
Here’s a simple decorator:
1 2 3 4 5 6 | def my_decorator(func): def wrapper(): print("Before the function runs") func() print("After the function runs") return wrapper |
Let’s break it down:
funcis the function being decoratedwrapper()adds extra behaviorfunc()calls the original function- The decorator returns the new function
Apply it manually:
1 2 3 4 5 | def greet(): print("Hello!") greet = my_decorator(greet) greet() |
Output:
1 2 3 | Before the function runs Hello! After the function runs |
The @decorator Syntax
Python provides a cleaner syntax using @:
1 2 3 | @my_decorator def greet(): print("Hello!") |
This is equivalent to:
1 | greet = my_decorator(greet) |
Much cleaner—and easier to read.
Decorators with Arguments
Most real functions accept parameters. Let’s update our decorator:
1 2 3 4 5 6 7 | def my_decorator(func): def wrapper(*args, **kwargs): print("Before function") result = func(*args, **kwargs) print("After function") return result return wrapper |
Now it works with any function:
1 2 3 4 5 | @my_decorator def add(a, b): return a + b print(add(3, 5)) |
Output:
1 2 3 | Before function After function 8 |
Preserving Function Metadata (functools.wraps)
Without extra care, decorators can overwrite:
- Function name
- Docstring
- Annotations
Example problem:
1 | print(add.__name__) # wrapper |
Fix it with functools.wraps:
1 2 3 4 5 6 7 | from functools import wraps def my_decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper |
Now metadata is preserved:
1 | print(add.__name__) # add |
Decorators with Arguments (Advanced)
Sometimes decorators themselves need parameters:
1 2 3 4 5 6 7 | def repeat(n): def decorator(func): def wrapper(*args, **kwargs): for _ in range(n): func(*args, **kwargs) return wrapper return decorator |
Usage:
1 2 3 4 5 | @repeat(3) def say_hi(): print("Hi!") say_hi() |
Output:
1 2 3 | Hi! Hi! Hi! |
Common Real-World Use Cases
Logging
1 2 3 4 5 | def log(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper |
Timing Function Execution
1 2 3 4 5 6 7 8 9 10 | import time def timer(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} took {end - start:.4f}s") return result return wrapper |
Authentication / Permissions
Decorators are commonly used in web frameworks like Flask and Django.
1 2 3 4 5 6 | def require_login(func): def wrapper(user): if not user.is_authenticated: raise PermissionError("Login required") return func(user) return wrapper |
Built-in Decorators You Already Use
Python includes many decorators out of the box:
@staticmethod@classmethod@property@dataclass@lru_cache
@lru_cache decorator in Python
Example:
1 2 3 4 5 6 7 | from functools import lru_cache @lru_cache def fibonacci(n): if n < 2: return n return fibonacci(n-1) + fibonacci(n-2) |
@classmethod decorator in Python
Example of using the @classmethod decorator in Python
Basic example
1 2 3 4 5 6 | class MyClass: value = 10 @classmethod def show_value(cls): return cls.value |
How it works
1 | print(MyClass.show_value()) # 10 |
@classmethodreceives the class itself as the first argument (cls)- You don’t need to create an instance
- It can access class variables (
cls.value)
Comparison with instance method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class MyClass: value = 10 def instance_method(self): return self.value @classmethod def class_method(cls): return cls.value obj = MyClass() print(obj.instance_method()) # 10 print(MyClass.class_method()) # 10 |
self→ instancecls→ class
Practical example: alternative constructor
This is the most common real-world use.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class FirePixel: def __init__(self, lat, lon, frp): self.lat = lat self.lon = lon self.frp = frp @classmethod def from_dict(cls, data): return cls( lat=data["latitude"], lon=data["longitude"], frp=data["frp"] ) |
Usage
1 2 3 4 5 | d = {"latitude": 34.2, "longitude": -118.4, "frp": 56.7} pixel = FirePixel.from_dict(d) print(pixel.lat, pixel.lon, pixel.frp) |
Why @classmethod here?
- Creates an instance without hardcoding the class name
- Works correctly if the class is subclassed
Subclassing benefit
1 2 3 4 5 | class VIIRSPixel(FirePixel): pass p = VIIRSPixel.from_dict(d) print(type(p)) # <class '__main__.VIIRSPixel'> |
If from_dict were a @staticmethod, this would not work properly.
When to use @classmethod
Use it when:
- You need access to the class, not an instance
- You want factory methods / alternative constructors
- You want code that respects inheritance
When NOT to use it
- If you only need instance data → use normal method
- If you don’t need
cls→ use@staticmethod
When Should You Use Decorators?
Use decorators when you want to:
- Add cross-cutting behavior (logging, timing, caching)
- Avoid repeating code
- Keep business logic clean
- Apply behavior consistently across functions
Avoid decorators when:
- The logic is highly specific to one function
- Readability would suffer
References
| Links | Site |
|---|---|
| Python Glossary – Decorator | python.org |
| Python Language Reference – Function Definitions | python.org |
| Python Standard Library – functools.wraps | python.org |
| Python Standard Library – functools.lru_cache | python.org |
| PEP 318 – Decorators for Functions and Methods | python.org (PEP) |
