Functions as objects is one of Python’s most powerful features — functions are first-class citizens, meaning they can be assigned to variables, passed as arguments, returned from other functions, stored in data structures, and treated like any other object (int, list, dict, etc.). This enables higher-order functions, callbacks, decorators, functional programming patterns, dynamic behavior, and code that is more flexible, reusable, and testable. In 2026, treating functions as objects remains essential — it powers decorators (logging, timing, caching), callbacks in GUIs/async I/O/ML pipelines, strategy patterns, dependency injection, and composable data transformations in pandas/Polars workflows. Understanding this unlocks cleaner, more Pythonic code.
Here’s a complete, practical guide to functions as objects in Python: assignment, passing as arguments, returning functions, storing in structures, real-world patterns (decorators, callbacks, strategies), and modern best practices with type hints, closures, functools, and pandas/Polars integration.
Assigning functions to variables — a function is just an object; its name is a reference.
def greet(name: str) -> str:
return f"Hello, {name}!"
hello = greet # hello now refers to the same function object
print(hello("Alice")) # Hello, Alice!
print(greet("Bob")) # Hello, Bob!
Passing functions as arguments — enables callbacks, higher-order functions, strategies.
def apply_operation(x: int, operation) -> int:
return operation(x)
def double(n: int) -> int:
return n * 2
def square(n: int) -> int:
return n ** 2
print(apply_operation(5, double)) # 10
print(apply_operation(5, square)) # 25
Returning functions from functions — factory pattern, closures, decorators.
def make_multiplier(factor: int):
def multiplier(n: int) -> int:
return n * factor
return multiplier
times3 = make_multiplier(3)
times5 = make_multiplier(5)
print(times3(10)) # 30
print(times5(10)) # 50
Storing functions in data structures — lists, dicts, sets — enables dynamic dispatch, plugin systems, strategy maps.
operations = {
"double": lambda x: x * 2,
"square": lambda x: x ** 2,
"negate": lambda x: -x
}
def compute(x: int, op_name: str) -> int:
return operations.get(op_name, lambda x: x)(x)
print(compute(5, "double")) # 10
print(compute(5, "square")) # 25
print(compute(5, "unknown")) # 5 (default identity)
Real-world pattern: functional transformations in pandas/Polars — pass functions for mapping, filtering, aggregation.
import pandas as pd
def normalize(col):
return (col - col.mean()) / col.std()
df = pd.DataFrame({'A': [1, 2, 3, 4], 'B': [10, 20, 30, 40]})
# Apply different functions dynamically
transformations = {
'normalized_A': normalize,
'doubled_B': lambda x: x * 2
}
for new_col, func in transformations.items():
df[new_col] = func(df[new_col.replace('_B', '') if '_B' in new_col else new_col])
print(df)
Best practices make function-as-object usage safe, readable, and performant. Use type hints — Callable[[int], int] or def func(op: Callable[[int], int]) -> int — improves clarity and mypy checks. Prefer named functions over lambdas for complex logic — easier to debug/test/document. Modern tip: use Polars expressions or pandas method chaining — DRY pipelines with function dispatch. Add closures for stateful functions — make_multiplier pattern. Use functools.partial — pre-bind arguments for reusable partials. Use decorators (@) — wrap functions for logging, timing, caching, validation. Store functions in dicts for strategy pattern — dynamic dispatch. Avoid excessive nesting — deep closures can hurt readability/performance. Test functions independently — pure functions are easiest. Combine with abc.ABC — define function protocols/interfaces for type safety. Use typing.Protocol — structural typing for callable objects. Use __call__ in classes — make objects callable like functions.
Functions as objects enable higher-order programming — assign, pass, return, store, and compose them like any value. In 2026, use type hints, prefer named functions, leverage decorators/partials, strategy dicts, and Polars/pandas functional patterns. Master functions as objects, and you’ll write flexible, composable, testable Python code that adapts elegantly to new requirements.
Next time you need dynamic behavior — treat functions as objects. It’s Python’s cleanest way to say: “Functions are data — pass them around, store them, create them on the fly.”