Python Decorators — 5 Advanced Features You Should Know

Mastering these under-the-hood capabilities will supercharge your functions, classes, and frameworks.

Python Decorators — 5 Advanced Features You Should Know
Photo by moren hsu on Unsplash

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 like tenacity 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.

Photo by Izuddin Helmi Adnan on Unsplash