Skip to content

#294: Callable Classes in Python

In Python, everything is an object - including functions. That flexibility goes both ways: not only can functions behave like objects, but objects can behave like functions. The mechanism that unlocks this duality is the special (or "dunder") method __call__. By defining __call__ in a class, we make it possible to invoke it with parentheses, just like an ordinary function, while still retaining all the advantages of stateful objects.

What exactly is __call__?

__call__(self, *args, **kwargs) is a method the interpreter looks for when we place (), i.e. the call operator, after an object. If it exists, Python executes that method instead of raising a TypeError. Conceptually, it is halfway between a regular method and operator overloading, something that other languages call a "functor".

1
2
3
4
5
6
class Adder:
    def __init__(self, n):
        self.n = n        # captured state

    def __call__(self, x):
        return x + self.n

We can now use our newly created class like this:

1
2
3
plus5 = Adder(5)
print(plus5(10))      # ➜ 15
print(plus5(21))      # ➜ 26

The line plus5(10) is syntactic sugar for plus5.__call__(10) but do only use the shortcut and not write the long variant.

Why should we use __call__?

The __call__ method allows us to invoke a class as if it was a regular function. This capability gives us these practical benefits:

  • Function-like behaviour with object-oriented structure: By implementing __call__, we create objects that we can use with function-call syntax (obj()), while still leveraging the features of classes such as encapsulation, inheritance, and composition.
  • Stateful and reusable components: __call__ makes it easy to build components that retain internal state across invocations. This allows us to bundle both data and behaviour into a single, reusable, and configurable object—useful in scenarios where a function alone would not be sufficient.
  • Common in advanced design patterns: Callable objects are frequently used in patterns such as decorators, event handlers, and callback systems. They are also a foundational concept in domains like machine learning, where models are often represented as objects that can be "called" with input data.

By understanding and using __call__, we can design more flexible, maintainable, and Pythonic interfaces in our applications and libraries.

A practical example: output cache for functions

We can create decorator to cache the output of a function that offers the same behaviour as we saw with the @cache decorator in post #210 with these few lines of code:

1
2
3
4
5
6
7
8
9
class CacheIt:
    def __init__(self, func):
        self.func = func
        self.cache = {}

    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)
        return self.cache[args]

We can now use our decorator whenever we have a computationally expensive function:

1
2
3
4
>>> @CacheIt
def fib(n):
     print(f"*** {n} ***")
     return n if n < 2 else fib(n-1) + fib(n-2)

The first call with a new argument computes it, while for every following call with the same argument we skip the computation and retrieve it from our cache.

When we run the fib() function with 3 as a parameter, it will print the different values down to 0 only the first time. The second time we call the function, it uses the cache and directly shows the result:

1
2
3
4
5
6
7
8
>>> fib(3)
*** 3 ***
*** 2 ***
*** 1 ***
*** 0 ***
2
>>> fib(3)
2

Caveats to remember

Before we jump in and put a __call__ method in all our classes, we need to talk about the downside:

  • Introspection: inspect.isfunction(obj) will return False for objects with __call__, so tooling needs to check callable(obj).
  • Readability: Overusing callable objects can blur the line between data and behaviour. Ask whether a plain function, closure, or even a @staticmethod would be clearer.
  • Pickling: If our callable object holds non‑picklable resources, distributing work across processes could break.

Conclusion

__call__ is a small hook with great potential. It let us blend object‑oriented structure with functional syntax, so that we can create APIs that are both expressive and intuitive. Understanding __call__ adds a subtle but powerful tool to our Python toolbox.