Back to Blog
Python

Python Callback Function: Passing Functions as Arguments

python callback function: Learn how Python callback functions work, how to pass functions as arguments, handle errors, and choose the right pattern for your code.

callbackshigher-order functionsfunction argumentsevent handlingasyncioerror handling
Illustration showing a function being passed as an argument and invoked by another function in Python

A python callback function is a function passed as an argument to another function, which invokes it at the appropriate time. This pattern appears throughout the standard library and in most third-party frameworks. The sorted function accepts a key callable, tkinter widgets take command callbacks, and asyncio relies on callbacks internally for event dispatch. Understanding how callbacks work in Python means understanding what happens when you pass a function reference, how the receiving function invokes it, and what constraints apply to the callback's signature.

How Function References Work in Python

In Python, functions are first-class objects. When you define a function with def, you create a callable object bound to a name. Passing that name to another function passes a reference, not a copy, and the receiving function can call it with () just like any other function.

def on_complete(result): print(f"Finished with {result}") def run_task(callback): result = 42 callback(result) run_task(on_complete)

The run_task function receives on_complete as the callback parameter and invokes it with one argument. Nothing special happens at the language level; the receiving function simply calls the object it was given. This is the entire mechanism behind callbacks in Python.

Passing Callbacks to Built-in Functions

The standard library uses callbacks in several places. The sorted function accepts a key callable that transforms each element before comparison. The filter and map built-ins accept a callable and an iterable. The functools.reduce function takes a binary callable and applies it cumulatively.

items = ["banana", "apple", "cherry", "date"] sorted_by_length = sorted(items, key=len)

Here len is the callback. The sorted function calls len on each item internally and sorts by the returned integer. You could pass any callable, including a lambda or a bound method, as long as it accepts one argument and returns a comparable value.

Callbacks in Event-Driven Code

Graphical toolkits and network libraries rely on callbacks to respond to events. In tkinter, a button's command option is a callback invoked when the user clicks. In asyncio, you can schedule a callback with loop.call_soon or loop.call_later.

import asyncio def log_event(message): print(f"Event: {message}") async def main(): loop = asyncio.get_running_loop() loop.call_soon(log_event, "task started") await asyncio.sleep(0) asyncio.run(main())

The callback log_event is scheduled to run on the next iteration of the event loop. It receives "task started" as its argument. Note that call_soon does not execute the callback immediately; it queues it. This distinction matters when you rely on ordering or timing in event-driven code.

Error Handling Inside Callbacks

When a callback raises an exception, the exception propagates to the code that invoked the callback. In a synchronous context, that means the calling function sees the exception. In an event loop, the exception is typically logged and the loop continues, which can make failures easy to miss.

def risky_callback(value): return 10 / value def safe_invoke(callback, *args): try: return callback(*args) except ZeroDivisionError as exc: print(f"Callback failed: {exc}") return None safe_invoke(risky_callback, 0)

If you are writing a library that accepts callbacks from user code, decide whether the callback's exceptions should propagate or be contained. Propagating gives the caller full control but can crash the calling context. Containing exceptions hides failures unless you log them. Most frameworks choose to catch and log, then continue processing other events.

Performance and Runtime Cost

Calling a callback in Python has the same cost as any ordinary function call: a frame is pushed, arguments are bound, and the function executes. The overhead is small but not zero. If you invoke a callback inside a tight loop over millions of items, the call overhead becomes measurable.

def double(x): return x * 2 values = range(1_000_000) result = list(map(double, values))

Using map with a Python-level callback is slower than a list comprehension that inlines the operation, because each iteration still goes through the function call machinery. For performance-sensitive paths, consider whether the callback can be replaced by an inlined expression or a built-in function implemented in C, such as operator.add or len.

Common Mistakes and How to Avoid Them

The most frequent mistake is passing the result of a function call instead of the function itself. button.config(command=handler()) invokes handler immediately and passes its return value, which is usually None. The fix is to pass handler without parentheses, or use a lambda when arguments are needed.

# Wrong: handler() runs immediately button.config(command=handler()) # Correct: pass the function reference button.config(command=handler) # Correct with arguments: use a lambda or functools.partial button.config(command=lambda: handler(item_id))

Another common issue is capturing loop variables in a lambda. A lambda closes over the variable, not its value, so all callbacks see the final loop value. Use a default argument to bind the current value.

callbacks = [] for i in range(3): callbacks.append(lambda i=i: print(i)) for cb in callbacks: cb()

The default argument i=i captures the current value at definition time, so the callbacks print 0, 1, and 2 instead of 2 three times.

Choosing Between Callbacks and Alternatives

Callbacks are not always the best design. If you need to chain multiple operations and handle results or errors at each step, an async function with await is often clearer than nested callbacks. If you need to transform data in a pipeline, generator expressions or list comprehensions are more readable than callbacks passed to map and filter.

Use a callback when the receiving function controls the timing of invocation, such as event handlers, sorting keys, or scheduling APIs. Use a direct function call when you control the flow yourself. Use asyncio coroutines when you need sequential asynchronous logic without nesting.

The decision depends on who owns the control flow. If the library or framework decides when to invoke your code, a callback is the natural interface. If you are writing the control flow, a direct call or an await expression is simpler and easier to debug.

python callback function: Practical Usage and Code Examples | RYUSLOG DEV