Python Function as Argument: Patterns and Pitfalls
Learn how to pass a Python function as an argument, including callbacks, higher-order functions, lambdas, and common mistakes.
Passing a function as an argument is a core Python pattern. In Python, functions are first-class objects, so you can pass them to another function just as you would pass an integer or a list. This pattern underpins callbacks, event handlers, and higher-order functions such as map and sorted.
The examples below cover the common patterns, the syntax for forwarding arguments, and the mistakes to avoid.
Functions as First-Class Objects
When you define a function with def, Python creates a callable object and binds it to a name. That object can be stored in a variable, placed in a data structure, or passed to another function. The function itself is not executed until you call it with parentheses. This distinction is important when passing a function as an argument: you pass the function object, not the result of calling it.
def greet(name): return f"Hello, {name}" # Passing the function object, not calling it alias = greet print(alias("Alice")) # Hello, Alice
Because functions are objects, they can be passed directly. The receiving function decides when and how to invoke them.
Passing a Function to Another Function
The simplest case is a function that accepts a callable and invokes it. This is often called a higher-order function. The caller supplies behavior, and the higher-order function controls the timing or context.
def apply_twice(func, value): return func(func(value)) def increment(x): return x + 1 print(apply_twice(increment, 5)) # 7
Here apply_twice receives increment as an argument and calls it twice. The key is that increment is passed without parentheses. This pattern is also used in decorators, where a function is wrapped and extended without modifying its source.
Using Callbacks for Event-Driven Code
Callbacks are functions passed to another piece of code so it can call them when an event occurs. This is common in GUI toolkits, network servers, and asynchronous frameworks. The callback is stored and invoked later, often with specific arguments.
def on_click(event): print(f"Clicked at {event}") def register_handler(handler): # In a real framework, this would be attached to a widget handler("button") register_handler(on_click)
When passing a callback, ensure the signature matches what the caller expects. If the callback needs extra parameters, use functools.partial or a lambda to adapt it.
from functools import partial def log(level, message): print(f"[{level}] {message}") info_log = partial(log, "INFO") info_log("Server started")
Higher-Order Functions in the Standard Library
Python's standard library uses function arguments extensively. map and filter accept a function that transforms or filters data, and sorted accepts a function through its key parameter. These functions work with any callable, including lambdas.
numbers = [1, 2, 3, 4] squared = list(map(lambda x: x * x, numbers)) even = list(filter(lambda x: x % 2 == 0, numbers)) sorted_by_abs = sorted([-3, 1, -2], key=abs)
sorted uses the key parameter to compute a sort key for each element. The key function is called once per element, so it should be efficient.
Function Arguments with *args and **kwargs
When writing a function that accepts another function, you often need to forward arguments. Using *args and **kwargs lets the wrapper pass any arguments through without knowing them in advance.
def call_with_logging(func, *args, **kwargs): print("Calling function") result = func(*args, **kwargs) print("Function finished") return result def add(a, b): return a + b print(call_with_logging(add, 3, 4)) # Calling function # Function finished # 7
This pattern is common in decorators and middleware. Because the wrapper forwards the same arguments, callers can usually use it as a drop-in replacement. If you are writing a decorator, use functools.wraps to copy the original function's metadata.
Lambda Functions as Short-Lived Arguments
Lambdas provide a concise way to define a function inline. They are useful when the function is small and will not be reused. However, lambdas can hurt readability if the logic is complex. Use them when the body fits on one line and the intent is clear.
# Clear and concise result = sorted(people, key=lambda p: p.age) # Hard to read result = sorted(people, key=lambda p: p.last_name.lower() + p.first_name.lower())
If the logic is more than a simple expression, define a named function instead. This also makes testing easier, because you can call the function directly.
Common Mistakes When Passing Functions
A frequent error is calling the function instead of passing it. When you write func() in the argument list, Python executes the function immediately and passes the result. This often leads to TypeError or unexpected behavior.
def call_twice(func): func() func() def say_hi(): print("hi") # Wrong: calls say_hi immediately, then tries to call None # call_twice(say_hi()) # Correct: pass the function object call_twice(say_hi)
Another mistake is assuming the callback will be called with specific arguments without verifying the signature. If the higher-order function passes arguments that your callback does not accept, Python raises a TypeError. Always check the expected signature or use *args to be flexible.
Performance and Maintainability Considerations
Passing a function as an argument has minimal overhead; the function object is just a reference, and calling it is the same as any other call. The real cost is the work the function performs.
Creating a lambda inside a loop allocates a new function object each time. In most programs this overhead is negligible, but in a hot loop it is worth reusing a named function when the logic does not depend on a changing closure value. More importantly, for nontrivial logic, named functions are easier to debug, test, and reuse than lambdas.
If an API accepts a callback, consider whether it should be called synchronously or asynchronously. If the callback may block, document that behavior. This is especially important in event loops, where a slow callback can stall the entire application.
Finally, remember that passing a function as an argument is a form of dependency injection. It lets you change behavior without modifying the calling code. This is a powerful tool for writing testable and extensible systems, but it also means the caller must understand the contract. Keep the contract simple and explicit.