Python Decorators — 5 Advanced Features You Should Know
Mastering these under-the-hood capabilities will supercharge your functions, classes, and frameworks.

They Look Like Magic — But They’re Pure Python
Python Decorators — 5 Advanced Features You Should Know
Decorators are among Python’s most powerful — and often misunderstood — features.
At first glance, they seem simple: a neat way to wrap functionality around other functions. But under the hood, decorators are a deep and flexible metaprogramming tool that powers everything from Flask routes to retry logic in distributed systems.
Most tutorials stop at the @wraps
and *args, **kwargs
level. But if you're an intermediate Python developer, there’s a next layer of insight waiting for you — a deeper understanding that can transform how you build tools, APIs, and internal frameworks.
In this article, we’ll explore 5 advanced features of Python decorators that go far beyond the basics. Whether you’re building a library, a CLI tool, or just want cleaner, more reusable code — these techniques are gold.
1. Decorators with Arguments (Decorator Factories)
Most people learn decorators like this:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
return wrapper
But what if you want to customize the decorator’s behavior?
Enter the decorator factory:
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello!")
greet()
# Hello! (printed 3 times)
You can now build decorators that take user-defined parameters — a foundational pattern in production codebases (e.g., @retry(times=5)
).
2. Class-Based Decorators (Stateful and Clean)
Sometimes, functions alone aren’t enough. If your decorator needs to track state, consider turning it into a class:
class CountCalls:
def __init__(self, func):
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
print(f"Call #{self.calls} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
Class-based decorators are perfect for memoization, caching, or analytics hooks — anywhere you need persistent state.
3. Preserving Metadata with functools.wraps
Decorators often strip away metadata like the function name and docstring. This can cause issues with introspection tools, documentation generators, or even some libraries.
from functools import wraps
def debug(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
Without @wraps
, tools like help()
or inspect
will show the wrong function name — often just wrapper
.
Always use @wraps
when writing decorators. It's a best practice and avoids painful debugging or broken tooling in the future.
4. Stacking Multiple Decorators and Their Order
You can stack multiple decorators like so:
@decorator_a
@decorator_b
def my_func():
pass
This is equivalent to:
my_func = decorator_a(decorator_b(my_func))
That means decorator_b
runs first, and decorator_a
wraps the result.
Here’s an example to clarify:
def dec1(f):
def wrapper(*args, **kwargs):
print("dec1")
return f(*args, **kwargs)
return wrapper
def dec2(f):
def wrapper(*args, **kwargs):
print("dec2")
return f(*args, **kwargs)
return wrapper
@dec1
@dec2
def test():
print("test")
test()
# Output:
# dec1
# dec2
# test
Decorator order can affect authorization, logging, exception handling, and more. Understanding this helps avoid subtle bugs.
5. Decorators for Class Methods and Classes Themselves
Decorators aren’t limited to functions — you can use them with class methods and even entire classes.
Decorating Methods:
Use @classmethod
or @staticmethod
— yes, they’re decorators too!
Custom example:
def log_method_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling method: {func.__name__}")
return func(*args, **kwargs)
return wrapper
class MyClass:
@log_method_call
def hello(self):
print("Hi!")
obj = MyClass()
obj.hello()
Decorating Classes:
You can also write decorators that take a class and return a modified class:
def add_repr(cls):
def __repr__(self):
return f"<{cls.__name__}: {self.__dict__}>"
cls.__repr__ = __repr__
return cls
@add_repr
class Person:
def __init__(self, name):
self.name = name
p = Person("Alice")
print(p) # <Person: {'name': 'Alice'}>
Decorating classes opens doors to ORMs, serializers, and even framework-level design.
Bonus: Real-World Examples of Advanced Decorator Use
- Flask/Django Routes
@app.route()
or@login_required
are decorators under the hood. - Retry Mechanisms
Tools liketenacity
let you do@retry(wait=2)
— built on decorator factories. - Permission Handling in APIs
@has_permission("admin")
— enables clean access control logic.
Wrapping Up
Decorators in Python are more than just a cute syntax trick. They’re a gateway into clean code, framework-level magic, and highly reusable patterns.
By mastering the advanced techniques covered here — from decorator factories to class-level decorators — you’ll unlock new levels of elegance and power in your Python codebase.
So the next time you find yourself repeating logic across functions, classes, or endpoints, ask yourself:
“Can this be a decorator?”
Chances are, it can — and should.
If this helped you level up your understanding of Python decorators, hit that clap button, share it with a friend, or follow me for more deep dives into Python’s hidden gems.
Until next time, keep wrapping those functions like a pro.
