Back to Blog
Python

Python Function as Argument: Patterns and Pitfalls

python function as argument: Learn how to pass a Python function as an argument, including callbacks, higher-order functions, lambdas, and common pitfalls.

higher-order functionscallbackslambdafunction arguments
A visual metaphor of a Python function being passed as an argument to another function, representing higher-order functions.

python function as argument requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In Python, functions are first-class objects. That means you can pass a function as an argument to another function, just like you would pass an integer or a list. This pattern is central to callbacks, event handlers, and higher-order functions like map and sorted. Understanding how to pass functions cleanly is a practical skill that appears in library design, asynchronous code, and everyday scripting.

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 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, filter, and sorted all accept a function that transforms or filters data. These functions are designed to 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. Avoid passing a function that performs heavy computation unless necessary.

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. It preserves the original function's signature, so callers can use the wrapper as a drop-in replacement.

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 functions as arguments has minimal runtime overhead. The function object is just a reference, and calling it is the same as any other call. The real cost comes from what the function does, not from the passing mechanism itself.

However, creating a lambda inside a loop can allocate a new function object each iteration. If the loop runs many times, this adds unnecessary allocation. In such cases, define the function once outside the loop.

# Avoid creating a lambda in a hot loop for i in range(1000): result = map(lambda x: x + i, data) # New lambda each iteration # Better: define a named function that captures i via argument def add_i(x, i): return x + i for i in range(1000): result = map(lambda x: add_i(x, i), data) # Still a lambda, but reuses add_i

For maintainability, prefer named functions when the logic is nontrivial. Named functions are easier to debug, test, and reuse. Lambdas are appropriate for short, obvious transformations. Also, document the expected signature of callbacks in docstrings, so other developers know what arguments to supply.

When designing an API that accepts a function, consider whether the function 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.

python function as argument: Practical Usage and Code Exampl | RYUSLOG DEV