First-Class Functions
In Python, functions are ordinary objects — they can be assigned to variables, stored in data structures, passed as arguments, and returned from other functions, just like any int or string.
Saying functions are 'first-class' in Python means a function is just another object: it has a type, it can be bound to a name, stored in a list or dict, passed into other functions, and returned as a result — with no special ceremony. This single property underlies decorators, callback-based APIs, functional-style tools like map/filter/sorted, and much of Python's flexibility. This topic focuses narrowly on that object-ness; decorators and closures as dedicated techniques are covered elsewhere.
Functions as Objects
A def statement creates a function object and binds it to a name in the current scope — exactly the same two-step process as x = 5 creates an int object and binds it to x. The function object has a type (function), attributes, and an identity, just like any other object.
def greet(name):
return f"Hello, {name}"
print(type(greet)) # <class 'function'>
print(greet.__name__) # 'greet'
print(callable(greet)) # True — it implements __call__Assigning Functions to Variables
Because greet is just a name bound to a function object, you can bind additional names to that same object — calling any of them calls the same underlying function.
Note the missing () — greet refers to the function object itself; greet() calls it. This distinction trips up beginners constantly.
def greet(name):
return f"Hello, {name}"
say_hi = greet # another name for the SAME function object
print(say_hi("Amara")) # Hello, Amara
print(say_hi is greet) # True — identical object, two namesStoring Functions in Data Structures
Functions can live inside lists, tuples, and especially dicts — a very common pattern for dispatch tables that replace long if/elif chains.
def add(a, b): return a + b
def sub(a, b): return a - b
def mul(a, b): return a * b
operations = {
"+": add,
"-": sub,
"*": mul,
}
def calculate(op, a, b):
return operations[op](a, b) # look up the function object, then call it
print(calculate("+", 3, 4)) # 7
print(calculate("*", 3, 4)) # 12Passing Functions as Arguments
A function that accepts another function as an argument is called a *higher-order function*. This is how sorted(key=...), map(), and filter() work, and it's the foundation of every decorator.
def apply_twice(func, value):
return func(func(value))
def increment(x):
return x + 1
print(apply_twice(increment, 5)) # 7 — increment(increment(5))
print(apply_twice(lambda x: x * 2, 3)) # 12Returning Functions from Functions
A function can construct and return another function, capturing arguments from the outer call in a closure. This is 'function factories' — one of the most powerful uses of first-class functions.
The closure mechanics here overlap with Scope & LEGB's Enclosing scope — see that topic for the full name-resolution picture.
def make_multiplier(factor):
def multiplier(x):
return x * factor # captures 'factor' from the enclosing scope
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5)) # 10 15Callable Objects
'Callable' is broader than 'function': any object implementing __call__ can be invoked with (), including class instances. This lets you build objects that behave like functions but carry their own persistent state.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
triple = Multiplier(3)
print(triple(7)) # 21 — called like a function
print(callable(triple)) # True
print(callable(42)) # False — an int is not callableHigher-Order Functions in the Standard Library
Python's built-ins and standard library lean heavily on functions-as-arguments: map, filter, sorted(key=...), and functools.reduce all take a function and apply it across data.
from functools import reduce
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]
total = reduce(lambda acc, x: acc + x, numbers, 0)
print(total) # 15Practical Usage
Dispatch tables (function-as-dict-value) replace unwieldy if/elif chains for command routing, event handlers, and strategy patterns. Passing functions as callbacks is standard in GUI frameworks, sorted/min/max key functions, and testing frameworks (fixtures, hooks).
handlers = {}
def on(event_name):
def register(func):
handlers[event_name] = func
return func
return register
@on("startup")
def handle_startup():
print("Starting up...")
handlers["startup"]() # Starting up...Edge Cases
Functions, being objects, support attribute assignment (you can attach custom attributes to a function object) and identity comparison with is, but two separately-defined functions with identical bodies are never == to each other — they're distinct objects.
def f(): pass
f.custom_tag = "important" # attaching an arbitrary attribute is legal
print(f.custom_tag) # important
def g(): pass
def h(): pass
print(g == h) # False — different function objects, even with identical bodiesMental Model
Stop thinking of a function definition as a special language construct and start thinking of it as 'create an object, bind it to a name' — no different from x = SomeClass(). Once that clicks, passing functions around, storing them, and returning them all feel as unremarkable as doing the same with a list or a string.
Quick Reference
def f(): ... # creates a function object, binds name 'f'
g = f # another name for the same object
d = {"key": f} # function stored as a dict value
h(f) # function passed as an argument (higher-order function)
def make():
def inner(): ...
return inner # function returned from a function
callable(obj) # True if obj implements __call__ (functions, methods, and instances with __call__)Key Takeaways
Functions are ordinary objects in Python — they can be assigned, stored, passed, and returned exactly like any other value.
A higher-order function either accepts a function as an argument, returns a function, or both.
Dispatch dicts mapping keys to function objects are an idiomatic replacement for long if/elif chains.
callable(obj) is broader than 'is a function' — any object implementing __call__ (including class instances) is callable.
Two distinct function objects are never == even with identical code — equality for functions is identity-based.
operations = {"+": add(), "-": sub()} # calls the functions immediately!
operations = {"+": add, "-": sub} # stores the function objects themselves
Adding () calls the function right away and stores its return value in the dict, not the function itself; omitting () stores a reference to the callable for later invocation.
def make_adder(n): return add(n) # calls add immediately instead of returning a function def make_adder(n): def add(x): return x + n return add # returns the function object, uncalled
To return a function that can be called later, return the function object itself (no parentheses) rather than the result of calling it now.
if callback == None: callback = default_handler result = callback == expected_handler # comparing function identity with == if callback is None: callback = default_handler result = callback is expected_handler
Function comparisons should use 'is' for identity (are these the same object?) since function objects don't define a meaningful __eq__ beyond identity — using == works but 'is' communicates intent correctly and is the idiomatic check against None.
What does it mean to say Python functions are 'first-class objects'?
It means a function is a regular object like any other value: it can be assigned to a variable, stored in a data structure, passed as an argument to another function, and returned as a result, with no special restrictions.
What is a higher-order function? Give an example from the standard library.
A function that takes another function as an argument, returns a function, or both. Examples: sorted(key=...), map(), filter(), and functools.reduce() all accept a function argument.
Why does writing operations['+']() versus operations['+'] behave so differently in a dispatch dict?
operations['+'] retrieves the function object stored under that key without calling it; adding () calls that retrieved function immediately. Confusing the two either fails to invoke the function when needed, or invokes it prematurely (e.g. at dict-construction time instead of dispatch time).
What makes an object 'callable' in Python, beyond ordinary functions?
Any object whose class implements the __call__ method is callable — this includes functions, methods, classes themselves (calling a class invokes its constructor), and class instances that define __call__, letting them be invoked with () like a function.
Why are two separately defined functions with identical bodies never equal with ==?
Function objects don't override __eq__ to compare source code or behavior — equality falls back to default identity comparison, so only a function compared to itself (or another name bound to the exact same object) is equal.
How does 'return a function from a function' relate to closures?
When the returned inner function references variables from the enclosing function's scope, it forms a closure — the returned function retains access to those enclosing variables even after the outer function has finished executing.
What is the practical benefit of a dispatch dict over a long if/elif chain for routing commands to handler functions?
It's more concise, avoids repetitive comparison logic, is O(1) lookup instead of a linear chain of comparisons, and makes adding new commands as simple as adding a new dict entry rather than another elif branch.
Write a function compose(f, g) that returns a new function equivalent to f(g(x)), then use it to compose 'add one' and 'double'.
def compose(f, g): def composed(x): return f(g(x)) return composed add_one = lambda x: x + 1 double = lambda x: x * 2 add_then_double = compose(double, add_one) print(add_then_double(5))
12 — add_one(5) is 6, then double(6) is 12; compose returns a new function applying g first, then f.
What does this print?
False then True — f and g are distinct function objects with identical bodies but different identities, so f == g is False; f == f compares an object to itself, which is True.
Write a dispatch-table-based calculator function calculate(op, a, b) supporting '+', '-', '*', '/' using a dict of lambdas.
operations = { "+": lambda a, b: a + b, "-": lambda a, b: a - b, "*": lambda a, b: a * b, "/": lambda a, b: a / b, } def calculate(op, a, b): return operations[op](a, b) print(calculate("*", 6, 7))
42 — the dict maps '*' to a lambda that multiplies its two arguments, and calculate() looks it up then calls it with a and b.
True then 15 — Adder instances are callable because the class defines __call__, and calling add5(10) runs __call__(add5, 10), returning 10 + 5.
def f():
return 1
def g():
return 1
print(f == g)
print(f == f)class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x + self.n
add5 = Adder(5)
print(callable(add5))
print(add5(10))