# Class-Based Python Decorators: When Functions Aren't Enough

You've built decorators with functions. You've nested them two layers deep, three layers deep, stacked them on top of each other. And it all works.

But at some point, you'll write a decorator that needs to remember things between calls: a counter, a cache, a log of every invocation... And you'll find yourself reaching `nonlocal`, juggling closure variables, and wondering if there'a a cleaner way.

There is. It's a class.

* * *

## A Quick Prerequisite: `__call__`

Before we build anything, you need to know one thing about Python classes: any object can behave like a function if its class defines a `__call__` method.

```python
class Greeter:
    def __call__(self, name):
        print(f"Hello, {name}!")

greet = Greeter()
greet("Moussa")  # Hello, Moussa!
```

`greet` is not a function, it's an instance of `Greeter`. But because `Greeter` has `__call__`, you can use parentheses on it as if it were a function. Python sees `greet("Moussa")` and internally calls `greet.__call__("Moussa")`.

This is the entire foundation of class-based decorators. If an object is callable, it can replace a function. And if it can replace a function, it can be a wrapper.

* * *

## Your First Class-Based Decorator

Let's start with something familiar, a decorator that logs every time a function is called:

**Function-based version (**[**what you already know**](https://blog.moussaamzat.dev/series/python-decorators)**):**

```python
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper
```

**Class-based version:**

```python
class LogCalls:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f"Calling {self.func.__name__}")
        return self.func(*args, **kwargs)

@LogCalls
def greet(name):
    return f"Hello, {name}"

print(greet("Moussa"))
# Calling greet
# Hello, Moussa
```

Let's trace through what Python does when it sees `@LogCalls`:

1.  Python calls `@LogsCall(greet)`: This triggers `__init__,` which stores the original function as `self.func`
    
2.  `greet` is now replaced by the `LogCalls` instance
    
3.  When you call `greet("Moussa")`, Python calls the instance, which triggers `__call__`
    
4.  Inside `__call__`, we run our logic and call `self.func("Moussa")`
    

Same pattern as before: take a function in, return something callable that wraps. The difference is that the "something callable" is now an object instead of an inner function.

* * *

## Why Bother? The State Problem

So far, the class version looks like more code for the same result. Fair enough 😀!  
The real advantage shows up when your decorator needs to maintain state.

Let's build a decorator that counts how many times a function has been called.

**Function-based version:**

```python
def count_calls(func):
    count = 0
    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        print(f"{func.__name__} has been called {count} time(s)")
        return func(*args, **kwargs)
    return wrapper
```

It works, but there's a problem: `count` is trapped inside the closure. You can't access it from outside. You can't reset it. You can't inspect it. It's invisible.

**Class-based version**

```python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} has been called {self.count} time(s)")
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hello, {name}"

greet("Moussa")   # greet has been called 1 time(s)
greet("Fabien")   # greet has been called 2 time(s)

print(greet.count)  # 2 — accessible!
greet.count = 0     # reset it
greet("Moussa")     # greet has been called 1 time(s)
```

The state lives on `self`, not inside a closure. You can read it, reset it, and even add methods to interact with it. The decorator becomes a proper object, not a black box.

This is the key insight: **function-based decorators use closure to remember state. Class-based decorators use** `self`. Same concept, different container, but `self` is far more accessible.

* * *

## Adding Methods to Your Decorators

Since the wrapper is now an object, you can give it useful methods:

```python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

    def reset(self):
        self.count = 0

    def report(self):
        print(f"{self.func.__name__} has been called {self.count} time(s)")

@CountCalls
def process_order(order_id):
    return f"Order {order_id} processed"

process_order(1)
process_order(2)
process_order(3)
process_order.report()  # process_order has been called 3 times
process_order.reset()
process_order.report()  # process_order has been called 0 times
```

Try doing that with a function-based decorator. You'd have to attach functions as attributes on the wrapper, awkward and messy. With a class, it's natural.

* * *

## Class-Based Decorators with Arguments

Just like function-based decorators, you can make class-based decorators configurable. The pattern shifts slightly.

**Without arguments**, `__init__` receives the arguments, and `__call__` receives the function:

```python
@MyDecorator(arg)   # Python calls MyDecorator(arg), then the result(func)
def my_function():
    pass
```

Here's a practical example: a decorator that slows down a function by a configurable number of seconds:

```python
import time

class SlowDown:
    def __init__(self, seconds):
        self.seconds = seconds

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print(f"Waiting {self.seconds}s before calling {func.__name__}...")
            time.sleep(self.seconds)
            return func(*args, **kwargs)
        return wrapper

@SlowDown(2)
def send_email(to):
    print(f"Email sent to {to}")

send_email("moussa@example.com")
# Waiting 2s before calling send_email...
# Email sent to moussa@example.com
```

Let's trace through it:

1.  Python evaluates `SlowDown(2)`: This creates an instance with `self.seconds = 2`
    
2.  Python calls that instance with the function: `instance(send_email)`, which triggers `__call__`
    
3.  `__call__` returns the `wrapper` function
    
4.  `send_email` is now replaced by `wrapper`
    

Notice the difference: when there are **no arguments**, `__call__` is the **wrapper itself** (it gets called every time the decorated function is called). When there **are arguments**, `__call__` is the **decorator** (it gets called once, receiving the function, and returns a wrapper).

This is the trickiest part of class-based decorators. Read that paragraph again if you need to.

* * *

## Practical Example: A Rate Limiter

Here's a class-based decorator that limits how often a function can be called:

```python
import time

class RateLimit:
    def __init__(self, min_interval):
        self.min_interval = min_interval

    def __call__(self, func):
        last_called = [0]  # using list to allow mutation in closure

        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[0]
            if elapsed < self.min_interval:
                wait = self.min_interval - elapsed
                print(f"Rate limited. Wait {wait:.1f}s")
                return None
            last_called[0] = time.time()
            return func(*args, **kwargs)
        return wrapper

@RateLimit(min_interval=2)
def call_api(endpoint):
    print(f"Calling {endpoint}")
    return {"status": "ok"}

call_api("/users")      # Calling /users
call_api("/users")      # Rate limited. Wait 1.8s
time.sleep(2)
call_api("/users")      # Calling /users
```

The `min_interval` configuration lives on the instance. The call timing state lives in the closure. This hybrid approach: class for configuration, closure for per-call state, is a common and effective pattern.

* * *

## Practice Example: A Retry with Exponential Backoff

```python
import time

class Retry:
    def __init__(self, max_attempts=3, backoff_factor=2):
        self.max_attempts = max_attempts
        self.backoff_factor = backoff_factor

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            for attempt in range(1, self.max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == self.max_attempts:
                        print(f"All {self.max_attempts} attempts failed.")
                        raise
                    wait = self.backoff_factor ** attempt
                    print(f"Attempt {attempt} failed: {e}. Retrying in {wait}s...")
                    time.sleep(wait)
        return wrapper

@Retry(max_attempts=3, backoff_factor=2)
def fetch_data(url):
    raise ConnectionError("Server unreachable")

# fetch_data("https://api.example.com")
# Attempt 1 failed: Server unreachable. Retrying in 2s...
# Attempt 2 failed: Server unreachable. Retrying in 4s...
# All 3 attempts failed.
```

The class makes the configuration readable: `max_attempts=3`, `backoff_factor=2` reads like a sentence. Compare this to the function-based version where these would be arguments to a factory function. Same result, but the class version communicates intent more clearly when the configuration is complex.

* * *

## When to Use Function-Based vs Class-Based

This isn't a "one is better" situation. They're tools for different contexts:

**Use function-based decorators when:**

*   Your decorator is simple: log, time, validate, then call the function
    
*   You don't need to maintain state between calls
    
*   You don't need to expose any interface to the caller
    
*   You want less boilerplate for a quick wrapper
    

**Use class-based decorators when:**

*   You need to maintain state across calls (counters, caches, history)
    
*   You want to expose methods or properties on the decorated function (`.reset()`, `.count`, `.report()`)
    
*   Your decorator has complex configuration with multiple parameters
    
*   You want to use inheritance to create decorator families
    
*   Readability matters more than brevity, classes make the structure explicit
    

Most decorators you'll write and encounter are function-based. But when the logic gets complex or stateful, classes keep things organized where closures start to get tangled.

* * *

## A Note on `functools.wraps`

One thing we haven't addressed: when you decorate a function with a class, the function's `__name__` and `__doc__` get replaced by the class's.

```python
@CountCalls
def greet(name):
    """Greet someone by name."""
    return f"Hello, {name}"

print(greet.__name__)  # 'CountCalls' — not 'greet'!
print(greet.__doc__)   # None — the docstring is gone!
```

For function-based decorators, you'd use `@functools.wraps(func)` on the wrapper.  
For class-based decorators, you can apply it in `__init__`:

```python
import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    """Greet someone by name."""
    return f"Hello, {name}"

print(greet.__name__)  # 'greet' — preserved!
print(greet.__doc__)   # 'Greet someone by name.' — preserved!
```

`functools.update_wrapper` copies the original function's metadata on to the instance. Always do this as it keeps debugging, documentation, and introspection tools working correctly.

* * *

## The Mental Model

Here's how function-based and class-based decorators map to each other:

**Function-based:**

*   Outer function receives the function (or arguments)
    
*   Inner function (wrapper) replaces the function
    
*   Closure variables are the state
    

**Class-based**

*   `__init__` receives the function (or arguments)
    
*   `__call__` replaces the function (or returns the wrapper)
    
*   `self` represents the state.
    

Same architecture, different syntax. If you understand one, you understand both. The only question is which one keeps your code for the specific problem you're solving.

* * *

## What's Next?

This article extends the Python Decorators series with a pattern you'll encounter in more advanced codebases, especially in frameworks, ORMs, and testing libraries. If you haven't read the earlier parts, here's the full series:

*   [Part 1: Understanding Python Decorators From Scratch](https://blog.moussaamzat.dev/understanding-python-decorators-from-scratch): The foundations
    
*   [Part 2: The Print vs Return Trap](https://blog.moussaamzat.dev/print-vs-return-trap-python-decorators): The most common silent bug
    
*   [Part 3: Advanced Patterns — Arguments and Stacking](https://blog.moussaamzat.dev/python-decorators-arguments-and-stacking): The real-world patterns
    
*   [Part 4: 10 Exercises to Master Decorators](https://blog.moussaamzat.dev/10-exercises-to-master-python-decorators): Hands-on practice
    

Thank you for reading 🙂.

* * *

> This is a bonus article in the Python Decorators series on Build, Break, Learn.  
> Written by a developer who learned the hard way so you don't have to.
