Python is the only language with idioms. This enhances its readability and, perhaps, its beauty. Decorators follow the Zen of Python, aka the ‘Pythonic’ way.
"There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch."
Decorators have been available since Python 2.2. They were enhanced with the PEP318.
Below is a beginner-focused tutorial for how to use decorators. If you prefer to run the code samples yourself, keep reading!

A decorator (not to be confused with the decorator pattern) is a method to add/change the behavior of functions without changing the original function. In Python, a decorator is a design pattern that allows you to modify the functionality of a function by wrapping it in another function. The outer function is called the decorator, which takes the original function as an argument and returns a modified version.
Let us learn by example. Here we declare a debug decorator. It helps print the output of the function instead of adding print commands, which later becomes unwieldy and, at times, irritatingly tedious to remove.
def debug(function):def wrapper(name, address):print ('Debugging:')func = function(name, address)print (func)return wrapper@debugdef typical_crunching_function(name, city):return 'You are '+ name + ' from '+ citytypical_crunching_function('John','Los Angeles')
Here we have defined the decorator in line 1-6 and applied to the function typical_crunching_function in line 8 using the @ syntax.
Class decorators were introduced in PEP3129. This was after a lot of resistance from the community which preferred metaclasses. The main purpose is to extend the ability of decorating functions to classes.
Here is an example of a class decorator enhancing the functionality of a function.
class Accolade:def __init__(self, function):self.function = functiondef __call__ (self, name):# Adding Excellency before namename = "Excellency " + nameself.function(name)# Saluting after the nameprint("Thanks "+ name+ " for gracing the occasion")@Accoladedef simple_function(name):print (name)simple_function('John McKinsey')
Here the Cleaner class is defined which can be used to perform pre-processing and post-processing to the simple_function. In this example we are simply adding Excellency to the string name and after printing the name we thanks for gracing the occasion. This simple example demonstrates that we can use class decorators to easily perform pre-processing and post-processing on the function arguments. These pre-processing tasks could be any of the below not limited to these.
Adding timing information
connecting to databases
closing connections
memoizing storage
While your example uses a class decorator implemented as a wrapper of a function, a powerful pattern is implementing the decorator itself as a class that accepts arguments, stores state, and defines __call__. Here's the template:
from functools import wrapsclass MyDecorator:def __init__(self, func=None, *, param1=None):self.func = funcself.param1 = param1def __call__(self, *args, **kwargs):if self.func is None:# used as @MyDecorator(param1=...), return a new instance wrapping the functionfunc = args[0]return type(self)(func, param1=self.param1)# actual call wrapper@wraps(self.func)def wrapper(*a, **kw):# pre-processing, can use self.param1print("Before call:", self.param1)result = self.func(*a, **kw)# post-processingprint("After call:", self.param1)return resultreturn wrapper(*args, **kwargs)
Usage examples:
# Without parameters@MyDecoratordef foo(x):return x * 2# With parameters@MyDecorator(param1=42)def bar(x):return x + 100
This pattern gives you rich flexibility: storing internal state, configurable behavior, and clean callable logic.
You should explain that class decorators implemented this way can elegantly support both parameterized and non-parameterized invocation, maintaining function metadata via wraps.
Here are some of the built-in class decorators.
Please note that its only available in Python >= 3.9. This allows caching previous values and reusing them instead of recalculating.
from functools import cache@cachedef factorial(n):return n * factorial(n-1) if n else 1print(factorial(10))
This decorator allows to add setter and getter functions to a property in a class.
class Pencil:def __init__(self, count):self._counter=count@propertydef counter(self):return self._counter@counter.setterdef counter(self, count):self._counter = count@counter.getterdef counter(self):return self._counterHB = Pencil(100)print (HB.counter)HB.counter = 20print (HB.counter)
This decorator allows the property of a class to be cached. This is equal to nesting the two decorators.
@cached@propertydef counter:return self._counter
Sometimes you want a class decorator that wraps every method on a class (logging, timing, access checks, etc.) without manually decorating each one. Here’s how you can approach it:
def decorate_all_methods(decorator_fn):def class_decorator(cls):for name, attr in cls.__dict__.items():if callable(attr) and not name.startswith("__"):setattr(cls, name, decorator_fn(attr))return clsreturn class_decorator# Example decorator functiondef log_calls(fn):@wraps(fn)def wrapped(*args, **kwargs):print(f"Calling {fn.__name__} with", args, kwargs)return fn(*args, **kwargs)return wrapped@decorate_all_methods(log_calls)class MyClass:def method1(self, x):return x * 2def method2(self, y):return y + 10
Caveats to discuss:
Special methods and dunder methods often should be skipped (you shouldn’t wrap __init__, __repr__ lightly).
This approach doesn’t catch dynamically added methods.
It may conflict with decorators already applied on some methods.
You might also combine this with metaclasses or use the descriptor protocol for full control.
In complex cases, a metaclass (or mix of decorator + metaclass) may be better.
Introducing this section helps bridge the gap between simple examples and real operational use.
Decorators are powerful, but class decorators come with trade-offs you should know:
Applied after class creation: Class decorators wrap the already built class object. They do not intervene in class creation itself (unlike metaclasses). PEP 3129 establishes this semantics.
Non-inheritance: If you decorate a base class, subclasses won’t automatically get that decoration. That is, class decorators do not propagate through class inheritance, unlike metaclasses.
Effect on introspection: Improper wrapping can obscure __name__, __doc__, __qualname__. Always use functools.wraps when wrapping methods inside the decorator.
Interaction with metaclasses: If a class already has a metaclass, combining with decorators must be done carefully. Decorators should respect the metaclass contract.
Order sensitivity: If multiple class decorators are stacked like @decA @decB class C, the application order is bottom up, same as function decorators.
Performance overhead: Wrapping many methods or heavy logic in __call__ may add runtime overhead and increase complexity.
State and concurrency: If your decorator stores mutable per-class state, be careful with thread safety or shared state across subclassing.
A balanced discussion helps developers avoid subtle bugs when applying class decorators in real systems.
Decorators are very convenient and elegant tools, However they are also very prone to errors. So they should be used with a lot of care.
Interactively Learn Advanced Python Concepts
This course will help you take the next level in your programming journey. It will demonstrate the ways you can use Python in your day-to-day work. We will cover intermediate and some advanced-level material in this course. Python 201 is split into four parts; Part one covers intermediate modules. Part two covers topics such as Unicode, generators, and iterators. Part three covers web-related tasks with Python. Part four covers testing code.