The Versatility of __call__: A Python Developer’s Secret Weapon

KitFu Coda
9 min read3 days ago

--

While reviewing the code in my current chatbot project, I discovered asyncio.to_thread accepts function parameters, removing the need to wrap the function with partial() as shown in the previous post. As a quick recap, wrapping a function with partial() allows a partial invocation of a function, by defining a subset of the arguments. The resulting callable can be called later with the remaining arguments (or further wrapped by additional partial() calls).

A cute sketch generated by Microsoft Copilot

Refactoring with __call__: From Personal Code to FastAPI

Consider the following fictional example,

def foo(arg_a, arg_b, arg_c):
...

Suppose we have both arg_b and arg_c defined, but we do not know about arg_a. If both arg_b and arg_c do not affect the behaviour of later computations, it would be nice to not having to keep passing the values around. Fortunately, Python allows the definition of higher-order functions. Higher-order functions are functions accepting functions as arguments. A form of encapsulation can then be achieved, by partially invoking function foo and pass the resulting callable objects to other functions.

from functools import partial

new_foo = partial(foo, arg_b="bar", arg_c="baz")

...

later_calculation(new_foo)

I take a lot of inspiration from functional programming paradigm in code. I certainly see some resemblance between partial() and a concept called currying. In languages that allow currying, if we invoke a function without all the arguments, it returns a callable. The invocation can be completed later with the callable with the remaining arguments. My favourite library toolz, which offers useful tools inspired by functional programming, has a function decorator that allows Python to mimic this behaviour.

Back to the refactoring exercise mentioned earlier. I mostly use Visual Studio Code for my work, and the powerful search function is useful to find all the lines calling the partial function. While removing the extra partial invocations for asyncio.to_thread calls, I realized I also use that a lot for other things. Not realizing it as a problem at first, I turned my attention towards the FastAPI application, seeking a way to reduce definition of global variables.

We are often reminded on why we should not define globals, but when used cautiously, it is a useful and convenient tool. For instance, we can setup a logger, and use it throughout the module to log useful information about program execution.

import logging

from fastapi import FastAPI

app = FastAPI()

logger = logging.getLogger(__name__)

@app.get('/')
def index() -> str:
logger.info("Received web request")

return "Hello world"

Suppose I want to reduce the use of globals, and only include logger in the request handler functions whenever needed. FastAPI offers dependency injection through the use of Depends function. The Depends function requires a callable, and FastAPI calls it whenever needed, but where do I pass the __name__ argument? Thankfully, this is addressed in the documentation in the advanced usage section.

Hint hint, it involves __call__. Adapting this to our example,

import logging
from typing import Any

from dataclass import dataclass
from fastapi import FastAPI, Depends

app = FastAPI()


@dataclass(frozen=True)
class get_logger:
name: str

def __call__(self) -> Any:
return logging.getLogger(self.name)


@app.get('/')
def index(logger: Any = Depends(get_logger(__name__))) -> str:
logger.info("Received web request")

return "Hello world"

It is indeed possible to write this with partial, as we discussed earlier. According to the discussion in this issue on github, all it takes is to rewrite get_logger as an async generator, as shown below.

from collections.abc import AsyncGenerator

async get_logger(name: str) -> AsyncGenerator[Any, None]:
yield logging.getLogger(name)


@app.get('/')
def index(logger: Any = Depends(partial(get_logger, __name__))) -> str:
logger.info("Received web request")

return "Hello world"

Now let us dial back a little bit and explain what __call__ is. It is a magic or dunder (short for double underscore) method in Python that makes objects instantiated from the class callable, as if they are a function. In our simplified example, the class is implemented as a dataclass to have the constructor generated automatically (frozen=True to allow FastAPI to cache the result). The callable object can also expect function parameters, as shown below,

@dataclass(frozen=True)
class get_logger:
name: str

def __call__(self, arg_a):
logger = logging.getLogger(self.name)
logger.info("Logger is initiated with arg_a:%s", arg_a)
return logger


logger_getter = get_logger(__name__)
logger = logger_getter("bar")

While I cannot demonstrate it in our simplified example, defining a callable as a class also brings benefits such as state management, seen in a normal class. It is also especially useful in cases where complex setup is needed.

Remember the refactoring exercise I mentioned earlier, where I found many uses of partial function calls in my code? The use of __call__ in my FastAPI application inspired me to look into other parts of the chatbot. Specifically, this section, which was also featured in the article I posted last week.

import signal
from functools import partial

def shutdown_handler(
_signum, _frame, exit_event: threading.Event
) -> None:
exit_event.set()

manager = multiprocessing.Manager()
exit_event = manager.Event()

for s in (signal.SIGHUP, signal.SIGTERM, signal.SIGINT):
signal.signal(
s, partial(shutdown_handler, exit_event=exit_event)
)

with ProcessPoolExecutor() as executor:
executor.submit(process_run, tg_run, exit_event)
executor.submit(process_run, web_run, exit_event)

In the snippet, shutdown_handler is a function I pass to signal.signal. According to the documentation, the handler function expects a signal number, as well as a stack frame. I needed it to send exit signals to processes whenever any of those signal is captured, hence I hacked exit_event into the handler with use of partial.

After the refactoring exercise done to the FastAPI application, I started to think if this is the best use of partial. In my production code, I also pass in some other arguments. Eventually, it becomes harder to recognize that the function is meant to be a handler as the list of arguments grows (increasing argument list sometimes also means that the function’s scope needs revision, but that’s a topic for another day).

I am breaking the naming convention on purpose, both to preserve the existing name and emphasize its callable nature. Putting naming issues aside for now, if we refactor the code to use __call__ it becomes

from types import FrameType
from dataclasses import dataclass

@dataclass
class shutdown_handler:
exit_event: Event

def __call__(self, signal_num: int | None, frame: FrameType | None) -> None:
self.logger.info(
"MAIN: Sending exit event to all tasks in pool",
signal=signal_num,
frame=frame,
)
self.exit_event.set()

...

with ProcessPoolExecutor(max_workers=5) as executor, suppress(queue.Empty):
for signal_num in (signal.SIGHUP, signal.SIGTERM, signal.SIGINT):
signal.signal(signal_num, shutdown_handler(exit_event))

Compared to the previous version, I find this significantly more readable and understandable. By reading the __call__ method, it becomes apparent that the callable object is written to act as a signal handler.

I briefly mentioned that defining a callable as a class implementing __call__ allow storing of states previously. Alternatively, I want to explore another approach to doing this, through genruler, the DSL library I recently reimplemented.

__call__ vs. Closures: A Genruler Exploration

Genruler is a DSL library, where it defines and parses a lisp-like domain-specifc language. Like other programming languages, it defines functions. As an example for our next discussion, we take a look at string.concat. It is mapped to a function written in Python, which is simplified below,

 def concat(link: str, *arguments: str) -> Callable[[dict[Any, Any]], str]:
def inner(context: dict[Any, Any]) -> str:
return link.join(arguments)

return inner

The function is written as a closure, according to MDN Web Docs (while the context is JavaScript, it applies to Python too),

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives a function access to its outer scope. In JavaScript, closures are created every time a function is created, at function creation time.

Genruler works by first parsing the code written in the DSL. Then it is only evaluated when a context is given (think of it as a source of data), but for our discussion it is inessential. We just need to know that evaluation only happens once a context is given.

Therefore, in our simplified example, it also follows the two stage procedure. When it is called with the link and a list of string (parsed and extracted from user’s code), it returns the inner function. When it comes to the time to evaluate, the inner function is called with the context. As mentioned, it is a string concatenation function, which is really just the string .join() method.

In this case, the function concat fits the definition of a closure, as its inner function does have access to the outer scope (link and arguments). This, is how state can be managed, albeit in a more limited capacity, in a function closure.

Genruler is an experimental project, as this reimplementation isn’t made to be used in a real project, unlike the previous incarnation. I tested some ideas while working on the library, and one of it was figuring out if it is possible to translate the closure back to the lisp-like syntax. It was inconclusive, but in one of my attempts, I rewrote a module to use __call__ dunder.

The rewrite is fairly simple, for example, the concat function we saw earlier, can be rewritten as,

class concat:
def __init__(self, link: str, *arguments: str):
self.link = link
self.arguments = arguments

def __call__(self, context: dict[Any, Any]) -> str:
return self.link.join(self.arguments)

Both code snippet achieve similar goals, my only nitpick is that for the function closure version, I cannot really rewrite inner as a lambda function. It would work for the example, but lambda in Python has a limitation where only one statement is allowed. Should the function contains more than one statement, it would fail.

On readability, I find both acceptable, but people who are not as familiar with functional programming may prefer the class-based syntax implementing __call__ dunder. In the end, it is a very subjective matter, depending on the environment where the code is deployed.

I still have not picked a favourite between the two type of representation. Functionally, they work the same. The compiled bytecode may show some difference in performance, but it is still too early to delve into the topic. The point of this article, is more on showcasing how __call__ can be an alternative.

The Power of __call__ and the Zen of Python

It is amazing how a seemingly irrelevant discovery eventually leads to the writing of this article. The refactoring exercise shows a pattern I find worthy of further observation. While Python supports certain functional programming construct, it is not designed for that (would be nice to have immutable dictionary, for instance). Therefore, in some cases, we could really consider writing callback functions, with __call__ dunder rather than partial.

The __call__ dunder can also be a viable alternative for function closures, as shown above. Combining the properties of a class, it allows for more flexibility, and can still be callable.

Photo by SaiKrishna Saketh Yellapragada on Unsplash

There is this saying, quoted from the Zen of Python, by Tim Peters

There should be one — and preferably only one — obvious way to do it.

Programming isn’t really just an exercise of expressing ideas in a way computers understand. More often than not, it is a tool that allow us to communicate our thought process. The fun thing (or otherwise) about having individual thought is that we may approach the same problem differently.

In one of my recent job interview, I was asked if I classify myself as a neurotypical. According to the interviewer, a person is considered neurotypical if his or her thoughts and views follows the consensus. It gets harder to classify myself thesedays, as the advance of technology makes it easier for us to express our thoughts. In fact, it takes more effort keeping things to oneself.

In such settings, it is hard to define what majority means in a world that is so diverse. I do find my opinion follows the crowd in one topic, but not some others. On the other hand, isn’t this is where innovation comes? I haven’t heard from my interviewer since, but I still find the conversation enlightening.

Back to the quote, there may be one true way to solve any given problem, but it doesn’t mean we shouldn’t explore. We now live in a world, where everyone is seeking for the quickest solution to problem. In the context of our discussion today, I hope I have done my job to show some of the possibilities __call__ dunder can bring.

That’s all I have for this week, thanks for reading, and I shall write again, next week.

This article received editorial assistance from a large language model to improve readability and to ensure clarity, while all content and code remain my own. I’m still open to project collaborations and job opportunities as the enlightening conversation didn’t lead to anything. Reach me via Medium or LinkedIn for more information.

--

--

KitFu Coda
KitFu Coda

Written by KitFu Coda

#coder, #blogger, #UnfitRunner, #php, #python3, #javascript, #Clojure, #UltimateFrisbee #boxing

No responses yet