Functions as variables (or functions as first-class objects) is one of Python’s most elegant and powerful features — functions are treated like any other object (int, str, list, dict, etc.), so you can assign them to variables, pass them as arguments, return them from other functions, store them in data structures, and use them dynamically. This enables higher-order functions, callbacks, decorators, strategy patterns, functional programming techniques, plugin systems, and code that adapts at runtime. In 2026, this capability remains central to clean, flexible, reusable Python code — powering decorators (logging, timing, caching), callbacks in GUIs/async/ML pipelines, dynamic dispatch in pandas/Polars transformations, and composable workflows in production systems.
Here’s a complete, practical guide to treating functions as variables 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’s name is just a reference; you can rebind it freely.
def greet(name: str) -> str:
return f"Hello, {name}!"
hello = greet # hello now points to the same function object
print(hello("Alice")) # Hello, Alice!
print(greet("Bob")) # Hello, Bob!
Passing functions as arguments — enables callbacks, higher-order functions, dependency injection, strategy patterns.
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, customizable behavior.
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:
func = operations.get(op_name, lambda x: x) # default identity
return func(x)
print(compute(5, "double")) # 10
print(compute(5, "square")) # 25
print(compute(5, "unknown")) # 5
Real-world pattern: functional transformations in pandas/Polars — pass functions for mapping, filtering, aggregation, dynamic processing.
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():
source_col = new_col.split('_')[1] if '_' in new_col else new_col
df[new_col] = func(df[source_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. Use 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.”