Using Python Class Decorators Effectively

Using Python Class Decorators Effectively

4 mins read
Oct 27, 2025
Share
Content
Decorators
Python Class Decorators
Using callable classes as decorators
Some Built-in Class Decorators
cache/ memoize
property
cached_property
Automatically decorating all methods of a class
Key limitations, pitfalls, and inheritance behavior
Conclusions

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!

Video thumbnail

Decorators#

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.

Python 3.5
def debug(function):
def wrapper(name, address):
print ('Debugging:')
func = function(name, address)
print (func)
return wrapper
@debug
def typical_crunching_function(name, city):
return 'You are '+ name + ' from '+ city
typical_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.

Python Class Decorators #

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.

Python 3.5
class Accolade:
def __init__(self, function):
self.function = function
def __call__ (self, name):
# Adding Excellency before name
name = "Excellency " + name
self.function(name)
# Saluting after the name
print("Thanks "+ name+ " for gracing the occasion")
@Accolade
def 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

Using callable classes as decorators#

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 wraps
class MyDecorator:
def __init__(self, func=None, *, param1=None):
self.func = func
self.param1 = param1
def __call__(self, *args, **kwargs):
if self.func is None:
# used as @MyDecorator(param1=...), return a new instance wrapping the function
func = args[0]
return type(self)(func, param1=self.param1)
# actual call wrapper
@wraps(self.func)
def wrapper(*a, **kw):
# pre-processing, can use self.param1
print("Before call:", self.param1)
result = self.func(*a, **kw)
# post-processing
print("After call:", self.param1)
return result
return wrapper(*args, **kwargs)

Usage examples:

# Without parameters
@MyDecorator
def 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.

Some Built-in Class Decorators#

Here are some of the built-in class decorators.

cache/ memoize #

Please note that its only available in Python >= 3.9. This allows caching previous values and reusing them instead of recalculating.

Python 3.10.4
from functools import cache
@cache
def factorial(n):
return n * factorial(n-1) if n else 1
print(factorial(10))

property#

This decorator allows to add setter and getter functions to a property in a class.

Python 3.10.4
class Pencil:
def __init__(self, count):
self._counter=count
@property
def counter(self):
return self._counter
@counter.setter
def counter(self, count):
self._counter = count
@counter.getter
def counter(self):
return self._counter
HB = Pencil(100)
print (HB.counter)
HB.counter = 20
print (HB.counter)

cached_property#

This decorator allows the property of a class to be cached. This is equal to nesting the two decorators.

@cached
@property
def counter:
return self._counter

Automatically decorating all methods of a class#

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 cls
return class_decorator
# Example decorator function
def 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 * 2
def 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.

Key limitations, pitfalls, and inheritance behavior#

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.

Conclusions#

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

Cover
Python 201 - Interactively Learn Advanced Concepts in Python 3

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.

11hrs
Intermediate
250 Playgrounds
22 Quizzes


Written By:
Zahid Irfan