How to Use Class Decorators in Python

In my years of developing enterprise-level Python applications, I’ve often found myself needing to add repetitive logic across multiple classes.

Whether it was for logging API calls or validating data schemas, manually editing every class felt like a waste of time.

That is where class decorators come into play. They are a powerful way to “wrap” a class and modify its behavior without changing its source code.

In this tutorial, I will show you exactly how to use class decorators in Python to write cleaner and more efficient code.

What is a Class Decorator in Python?

A class decorator is simply a function that receives a class as an argument and returns a new or modified version of that class.

If you have used function decorators before, the syntax @decorator_name will look very familiar to you.

The main difference is that instead of wrapping a function, we are applying the logic to an entire class structure.

Method 1: Use a Function as a Class Decorator

The easy way I’ve found to implement a class decorator is by using a standard function.

This is perfect when you want to add a new attribute or a simple method to every class in a specific module.

Suppose you are building a fleet management system for a logistics company based in Chicago. You want to ensure every vehicle class has a tracking ID.

Here is how I would implement that using a function-based decorator:

def assign_fleet_id(cls):
    # This function adds a fleet_id attribute to the class
    cls.fleet_id = "US-CH-9900"
    return cls

@assign_fleet_id
class DeliveryTruck:
    def __init__(self, driver_name):
        self.driver_name = driver_name

@assign_fleet_id
class MaintenanceVan:
    def __init__(self, technician):
        self.technician = technician

# Testing the implementation
truck = DeliveryTruck("John Smith")
van = MaintenanceVan("Alice Johnson")

print(f"Truck Driver: {truck.driver_name}, Fleet ID: {truck.fleet_id}")
print(f"Van Technician: {van.technician}, Fleet ID: {van.fleet_id}")

I executed the code above and added the screenshot below.

Class Decorators in Python

In this example, the assign_fleet_id function takes the class (either DeliveryTruck or MaintenanceVan) and injects the fleet_id property.

I find this much cleaner than using inheritance, especially when the added functionality doesn’t represent a “is-a” relationship.

Method 2: Create a Class Decorator with Arguments

Sometimes, a static decorator isn’t enough. You might need to pass specific configuration values to the decorator.

I often use this approach when setting up database connections for different US state offices, where each office has a different tax rate or ID.

To do this, you need to create a decorator factory, a function that returns the actual decorator.

def regional_tax_config(state_code, rate):
    def wrapper(cls):
        cls.state_code = state_code
        cls.tax_rate = rate
        
        # Adding a method to calculate total price including state tax
        def calculate_total(self, price):
            return price + (price * self.tax_rate)
        
        cls.calculate_total = calculate_total
        return cls
    return wrapper

@regional_tax_config("NY", 0.0887)
class NewYorkStore:
    def __init__(self, item_name):
        self.item_name = item_name

@regional_tax_config("TX", 0.0625)
class TexasStore:
    def __init__(self, item_name):
        self.item_name = item_name

# Real-world usage
ny_sale = NewYorkStore("MacBook Pro")
tx_sale = TexasStore("MacBook Pro")

print(f"New York Total for {ny_sale.item_name}: ${ny_sale.calculate_total(2000):.2f}")
print(f"Texas Total for {tx_sale.item_name}: ${tx_sale.calculate_total(2000):.2f}")

I executed the code above and added the screenshot below.

Use Class Decorators in Python

By passing “NY” and “TX” into the decorator, I can customize the behavior of each class at the time of definition.

Method 3: Use a Class as a Decorator

While functions are great, I sometimes prefer using a class as a decorator when the logic involves maintaining a state.

This requires implementing the __init__ and __call__ magic methods within the decorator class itself.

Imagine you are managing a real estate application in New York City, and you want to track how many times a property listing class is instantiated.

class InstanceCounter:
    def __init__(self, cls):
        self.cls = cls
        self.count = 0

    def __call__(self, *args, **kwargs):
        # Every time the class is called to create an instance, increment count
        self.count += 1
        instance = self.cls(*args, **kwargs)
        instance.instance_number = self.count
        print(f"Total listings created in Manhattan: {self.count}")
        return instance

@InstanceCounter
class ManhattanListing:
    def __init__(self, address, price):
        self.address = address
        self.price = price

# Creating instances
p1 = ManhattanListing("5th Ave, NY", 1200000)
p2 = ManhattanListing("Wall St, NY", 3500000)
p3 = ManhattanListing("Broadway, NY", 950000)

print(f"Property at {p2.address} is listing number {p2.instance_number}")

I executed the code above and added the screenshot below.

How to Use Class Decorators in Python

In my experience, this is the most robust way to handle complex decorators that need to “remember” data across different parts of your application.

Why Should You Use Class Decorators?

You might wonder why we don’t just use standard inheritance or Mixins.

I’ve found that class decorators offer a few distinct advantages in real-world Python development.

First, they avoid the “diamond problem” associated with multiple inheritance, which can make your code very hard to debug.

Second, they allow you to modify a class dynamically at runtime, which is incredibly useful for framework development or plugin systems.

Finally, decorators keep your class hierarchy flat and readable, which is a core principle of the Zen of Python.

Common Use Cases for Class Decorators

In the professional projects I’ve worked on, I typically see class decorators used for:

  • Access Control: Restricting access to certain classes based on user roles (e.g., Admin vs. Guest).
  • Data Validation: Automatically checking if the attributes of a class meet specific criteria.
  • Singleton Pattern: Ensuring that only one instance of a class ever exists (common for database drivers).
  • Registering Classes: Automatically adding a class to a registry or a factory map upon definition.

Handle Class Decorators with Metadata

One thing I always remind junior developers is to be careful about metadata when using decorators.

When you wrap a class, you might lose information like the original class name or the docstring.

To fix this, I recommend using functools.wraps, although it is more common in function decorators. For classes, you manually ensure __name__ and __doc__ are preserved.

def preserve_metadata(cls):
    # Logic to modify the class
    cls.is_decorated = True
    return cls

@preserve_metadata
class UserProfile:
    """Class representing a US-based user profile."""
    pass

print(f"Class Name: {UserProfile.__name__}")
print(f"Documentation: {UserProfile.__doc__}")

As long as you return the original class object (even if modified), the metadata stays intact.

Difference Between Function Decorators and Class Decorators

While the syntax is the same, the execution logic differs slightly.

A function decorator wraps the execution of a function call.

A class decorator wraps the creation of the class itself.

In my daily workflow, I use function decorators for behavior that happens during a task, and class decorators for behavior that defines what an object is.

Debug Class Decorators

Debugging decorators can be tricky because the error might point to the decorator line rather than the class definition.

I suggest using a debugger like the one in VS Code or PyCharm to step into the decorator function.

Checking the __mro__ (Method Resolution Order) of your instances can also help you see if a decorator has unexpectedly changed the class hierarchy.

Real-World Example: Logging US Census Data Access

Let’s look at a more complex example. Suppose you are working with a government API that provides US Census data.

You want to log every time a data-access class is created for auditing purposes.

import datetime

def audit_logger(cls):
    original_init = cls.__init__

    def new_init(self, *args, **kwargs):
        print(f"[AUDIT] Accessing {cls.__name__} at {datetime.datetime.now()}")
        print(f"[AUDIT] Arguments: {args} {kwargs}")
        original_init(self, *args, **kwargs)

    # Replace the original __init__ with our audited version
    cls.__init__ = new_init
    return cls

@audit_logger
class CensusData:
    def __init__(self, region, year):
        self.region = region
        self.year = year

# Accessing the data
data_ca = CensusData("California", 2024)
data_fl = CensusData("Florida", 2024)

In this scenario, I’ve used a technique called “monkey patching” within the decorator to swap the constructor method.

This allows me to inject logging logic into any class without the user of the class even knowing it exists.

Final Thoughts on Python Class Decorators

Class decorators are an advanced tool, but they aren’t nearly as intimidating as they first appear.

If you understand that a decorator is just a function that takes a class and returns a class, you have already mastered the hard part.

I hope these examples and my personal experiences help you write cleaner, more Pythonic code in your next project.

I have used these techniques in countless US-based tech projects to save time and reduce code duplication.

You may also like to read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.