Use Python Functions as Callback Arguments

Learn how to pass Python functions as callbacks, how callback patterns work in event-driven and asynchronous code, and when to use callbacks versus other approaches.

7 min read

A callback is a function that you pass to another piece of code with the understanding that it will be called later, when a specific condition is met or a specific event occurs. The code that receives the callback does not know what the callback does internally. It only knows when to invoke it and what arguments to pass. This inversion of control, where the caller defines the behavior but the receiver decides when to trigger it, is the essence of the callback pattern and one of the most practical applications of Python's first-class functions.

The article on higher-order functions in Python covered functions that accept other functions as arguments. Callbacks are a specialized case of this pattern where the timing of the call is the key aspect. When you pass len to sorted as the key argument, sorted calls len immediately during the sorting process, and the call happens before sorted returns. When you pass a callback to a function that starts a network request, the callback might not be called for several seconds, and it might be called from a different thread. The temporal separation between passing the callback and having it invoked is what distinguishes callbacks from other function arguments.

Callbacks for customizing behavior

The simplest callback pattern is providing a function that customizes how another function operates. Sorted with a key argument is a callback, though the term is more often reserved for cases where the callback is the primary mechanism of customization. A function that processes a list of files might accept a callback that is called for each file, letting the caller decide whether to count it, analyze it, or skip it:

pythonpython
def process_files(filenames, callback):
    results = []
    for filename in filenames:
        result = callback(filename)
        if result is not None:
            results.append(result)
    return results
 
def extract_extension(filename):
    return filename.split(".")[-1] if "." in filename else None

The process_files function iterates through the filenames and calls the callback parameter on each one, collecting the non-None results into a list. The caller passes a function like extract_extension to get file extensions, or a different callback to get file sizes or filtered results. The iteration logic is written once in the higher-order function, and the per-file behavior is pluggable through the callback, a pattern also covered in the article on writing reusable Python functions.

This pattern appears throughout Python's standard library and third-party packages. Any function that traverses a data structure, iterates over a collection, or handles a stream of events can benefit from accepting a callback that lets the caller inject behavior at each step without modifying the traversal or iteration code.

Callbacks for event handling

Event-driven programming relies on callbacks as its core mechanism. A graphical user interface framework registers callback functions for events like button clicks, key presses, and window resizes. The framework's event loop waits for events, and when one occurs, it looks up the registered callback and calls it with information about the event. The callback does not need to know how the event loop works; it only needs to handle the specific event it was registered for:

pythonpython
class Button:
    def __init__(self):
        self._on_click = None
 
    def on_click(self, callback):
        self._on_click = callback
 
    def click(self):
        if self._on_click:
            self._on_click()

A Button instance stores a callback in the attribute _on_click. When the click method is called, perhaps by a GUI framework's event loop, the stored callback is invoked. The Button does not know what the callback does; it only knows to call it when clicked. A caller registers a handler with button.on_click(handle_click), where handle_click is a function like def handle_click(): print("Clicked!"), and that handler fires whenever button.click() runs.

The Button does not know or care what handle_click does. It only knows that a callback was registered and should be called when the button is clicked. This separation of concerns is what makes event-driven systems composable: the button handles input detection, and the callback handles the application's response.

Callbacks for asynchronous operations

Before Python's async and await syntax became widely available, callbacks were the primary mechanism for handling asynchronous results. A function that starts a long-running operation, like reading a file or making an HTTP request, accepts a callback to call when the operation completes:

pythonpython
def fetch_data(url, on_success, on_error):
    try:
        response = perform_request(url)
        on_success(response)
    except Exception as e:
        on_error(e)

Two callbacks are provided: one for success and one for failure. The fetch_data function decides which to call based on the outcome of the request.

In modern Python, async and await have largely replaced explicit callbacks for asynchronous code because they produce code that reads sequentially rather than being split across callback functions. The article on asynchronous programming later in this learning path covers this evolution in detail. Callbacks remain the underlying mechanism that many async frameworks use internally, and understanding callbacks is still necessary for working with libraries and systems that expose callback-based APIs.

Designing functions that accept callbacks

When you write a function that accepts a callback, document three things clearly. First, when will the callback be called? Is it called synchronously before your function returns, or asynchronously at some later time? Second, what arguments will the callback receive? The callback's parameter list must match what your function provides. Third, what should the callback return, and what will your function do with that return value? If the return value is ignored, say so. If None has a special meaning, document it.

Make the callback optional when reasonable by providing a default of None and checking for it before calling:

pythonpython
def process_items(items, on_each=None):
    results = []
    for item in items:
        if on_each:
            result = on_each(item)
        results.append(result)
    return results

A function that accepts an optional progress callback lets callers that want progress updates provide one while callers that do not care can omit it. The default behavior should be to skip the callback entirely rather than to call a no-op function, because checking for None is simpler and clearer than defining and passing a do-nothing lambda.

Rune AI

Rune AI

Key Insights

  • A callback is a function passed as an argument to be called later, typically when an event occurs or a task completes.
  • Callbacks rely on Python's first-class functions and are a specific application of higher-order functions.
  • The sorted key parameter, GUI event handlers, and asynchronous completion handlers are all callback patterns.
  • In modern Python, async and await often replace explicit callbacks for asynchronous code.
  • When writing a function that accepts a callback, document when and with what arguments the callback will be invoked.
RunePowered by Rune AI

Frequently Asked Questions

What is a callback function in Python?

A callback is a function that is passed as an argument to another function and is called by that function when a specific event occurs or a task completes. The function that receives the callback does not know in advance what the callback does; it only knows when to call it. Callbacks are the standard pattern for handling asynchronous results, user interface events, and customizable behaviors in libraries.

How is a callback different from a regular function argument?

Technically, there is no difference. A callback is a function argument, but the term callback emphasizes that the function will be called later, often asynchronously, in response to an event. When you pass len to sorted as the key argument, you are passing a function argument. When you pass a function to be called when a network request completes, that function argument is specifically called a callback.

What are the alternatives to callbacks in modern Python?

For asynchronous code, async and await with coroutines have largely replaced explicit callback patterns because they produce code that reads sequentially rather than being split across callback functions. For customizing behavior, dependency injection and configuration objects are alternatives to callbacks. Callbacks remain the right choice when the calling code genuinely does not know when or if the callback will be invoked, such as in event-driven systems.

Conclusion

Callbacks are the mechanism by which one piece of code says to another: here is a function, call it when you are ready. They are the natural extension of first-class functions and higher-order functions into the domain of event-driven and asynchronous programming. Understanding callbacks prepares you for both the traditional callback-based APIs you will encounter in Python libraries and the async and await patterns that build on the same conceptual foundation.