Decorators, callbacks, map(), sorted() with a key argument, event handlers in web frameworks -- all of these depend on the same underlying rule. That rule is not a special feature bolted onto Python's function system. It is a consequence of a single design decision made at the language's foundation: functions are objects. This article traces that rule from the ground up, through the object model, the callable protocol, and into the practical patterns it makes possible.
In languages like C, functions and data live in separate worlds. A function exists at a fixed address in memory, and you pass it around using a function pointer, which is a different kind of thing from a regular variable. In Python, there is no such separation. A function defined with def produces a value -- an object -- that is assigned to a name in the current scope, just like x = 42 assigns an integer object to the name x. That uniformity is the entire foundation.
Everything Is an Object, Including Functions
In Python, every piece of data is an object. Every object has three properties: an identity (its address in memory, retrievable with id()), a type (retrievable with type()), and a value. This applies to integers, strings, lists, and crucially, to functions.
def greet(name):
"""Return a greeting string."""
return f"Hello, {name}"
# A function has an identity (memory address)
print(id(greet)) # e.g., 140234866219040
# A function has a type
print(type(greet)) #
# A function has attributes, just like any object
print(greet.__name__) # greet
print(greet.__doc__) # Return a greeting string.
print(greet.__code__) #
When Python executes the def greet(name): statement, it does not just register a function somewhere. It creates a function object, compiles the body into a code object stored in greet.__code__, and binds the resulting function object to the name greet in the current namespace. The name greet is a variable, and its value is a function object.
This means functions participate in the same name-binding mechanics as every other value. You can assign a function to a new name, delete the original, and call through the alias — the object is the same throughout:
def greet(name):
return f"Hello, {name}"
# Assign the function object to another name
say_hello = greet
# Both names reference the same object
print(say_hello is greet) # True
print(id(say_hello) == id(greet)) # True
# Calling through either name executes the same function
print(say_hello("Alice")) # Hello, Alice
print(greet("Alice")) # Hello, Alice
# Delete the original name; the object survives
del greet
print(say_hello("Bob")) # Hello, Bob
After del greet, the original name is gone, but the function object itself remains alive because say_hello still references it. The object persists as long as at least one reference to it exists. This is the same reference-counting behavior that applies to every Python object.
Under the hood in CPython, every Python object (including functions) is represented by a C struct that begins with a PyObject header containing two fields: ob_refcnt (the reference count) and ob_type (a pointer to the object's type). This uniform representation is what makes "everything is an object" possible at the implementation level. (Source: CPython C API — Common Object Structures.)
What a Function Object Carries
Python's function type is more than a callable wrapper around bytecode. A function object created by def carries a fixed set of attributes that expose its internals:
def greet(name: str) -> str:
"""Return a greeting string."""
language = "Python"
return f"Hello, {name} — from {language}"
# Identity and type (same as any object)
print(id(greet)) # memory address of the function object
print(type(greet)) # <class 'function'>
# Bytecode and metadata
print(greet.__name__) # greet
print(greet.__qualname__) # greet (nested functions show "Outer.inner")
print(greet.__doc__) # Return a greeting string.
print(greet.__module__) # __main__ (or the module it was defined in)
print(greet.__code__) # <code object greet at 0x...>
# Compiled bytecode internals
print(greet.__code__.co_varnames) # ('name', 'language') — local variable names
print(greet.__code__.co_consts) # (None, 'Return a greeting string.', 'Python')
print(greet.__code__.co_argcount) # 1 — number of positional arguments
# Type annotations (stored on the function object, not inside __code__)
print(greet.__annotations__) # {'name': <class 'str'>, 'return': <class 'str'>}
# The namespace the function closes over (its global scope)
print(type(greet.__globals__)) # <class 'dict'> — the module's global namespace
# Closure cells (None for a non-closure function)
print(greet.__closure__) # None
The separation between __code__ (the compiled bytecode, shared if two functions have identical bodies) and __globals__ (the live namespace the function executes in) is what allows Python to handle module-level functions correctly: the bytecode is fixed at compile time, but the function object is a runtime artifact that carries a reference to its defining scope. This is also why two def statements with identical bodies produce two different function objects — they share the code object but hold references to the same globals dict, and they have separate identities.
The intellectual foundation for treating functions as values was laid by Christopher Strachey at the International Summer School in Computer Programming, Copenhagen, in August 1967. In notes that circulated privately for decades before being formally published, Strachey introduced the distinction between "functions and procedures" as first-class and second-class objects in a language. His paper, eventually published in Higher-Order and Symbolic Computation (Vol. 13, 2000, pp. 11–49), also introduced the concept of the closure as the "R-value of a function" — that is, the pair of a function's code and its environment. Python's function object is a direct descendant of that idea.
type(greet) return when greet is a function defined with def?method is a distinct type in Python — it wraps a function and binds it to an instance. A standalone function defined with def at module or local scope is not a method; it is a plain function object. Run type(greet) and you will see <class 'function'>, not <class 'method'>.def greet(name):
return f"Hello, {name}"
print(type(greet)) # <class 'function'>
class Greeter:
def greet(self, name):
return f"Hello, {name}"
g = Greeter()
print(type(g.greet)) # <class 'method'>
# Accessing greet through an instance produces a bound method,
# not a plain function. That is why the types differ.
def is an instance of the built-in function class. Calling type(greet) returns <class 'function'>. This confirms that functions are genuine objects with a type, just like integers (<class 'int'>) or strings (<class 'str'>). Because they are objects, they can be assigned to other names, stored in containers, and passed as arguments.def greet(name):
return f"Hello, {name}"
print(type(greet)) # <class 'function'>
print(type(42)) # <class 'int'>
print(type("hello")) # <class 'str'>
# Functions, integers, and strings are all objects.
# type() works the same way on all of them.
callable in Python. callable is a built-in function that checks whether an object can be called, but it is not a type name. The actual type of a function defined with def is function. You can verify this: type(greet).__name__ returns 'function'.def greet(name):
return f"Hello, {name}"
print(type(greet)) # <class 'function'>
print(type(greet).__name__) # function
print(callable(greet)) # True — callable() is a function, not a type
# callable() returns True because greet has __call__, not because
# its type is named 'callable'.
Reference vs Call: The Parentheses Rule
There is a critical syntactic distinction between referencing a function and calling a function. Writing the name without parentheses gives you the object. Writing it with parentheses executes the object and gives you its return value. Getting this wrong is the most common mistake when working with higher-order functions.
def compute():
return 42
# Reference: the function object itself
print(compute) #
print(type(compute)) #
# Call: the function's return value
print(compute()) # 42
print(type(compute())) #
When you pass a function as an argument, you always pass the reference (without parentheses). If you accidentally include parentheses, you pass the result of calling the function, not the function itself:
def double(x):
return x * 2
def apply(func, value):
"""Call func with value and return the result."""
return func(value)
# Correct: pass the function object
result = apply(double, 5)
print(result) # 10
# Wrong: pass the return value of double(5), which is 10
# apply receives 10 (an int), then tries to call 10(5)
# result = apply(double(5), 5) # TypeError: 'int' object is not callable
This distinction is the bridge between "functions are objects" and "functions can be passed as arguments." Because double without parentheses evaluates to a function object, and function objects are values, they can be passed anywhere any other value can be passed.
lambda: Anonymous Function Objects
The lambda keyword creates a function object without giving it a name. A lambda expression produces exactly the same kind of object as def — an instance of the function class — but it is constrained to a single expression whose value becomes the implicit return value. For a full comparison of the two, see Python lambda vs def: every difference that actually matters.
# These two are equivalent function objects:
def double_def(x):
return x * 2
double_lam = lambda x: x * 2
print(type(double_def)) # <class 'function'>
print(type(double_lam)) # <class 'function'>
# Both are callable and produce the same result
print(double_def(5)) # 10
print(double_lam(5)) # 10
# The difference is the __name__ attribute
print(double_def.__name__) # double_def
print(double_lam.__name__) # <lambda>
Because a lambda expression evaluates to a function object, it can be passed as an argument inline, without first assigning it to a name. This is the primary use case for lambda — short callables used once at the call site:
pairs = [(3, "cat"), (1, "ant"), (2, "bee")]
# Pass a lambda inline as the key — no separate def needed
sorted_pairs = sorted(pairs, key=lambda pair: pair[0])
print(sorted_pairs) # [(1, 'ant'), (2, 'bee'), (3, 'cat')]
# Equivalent with def — more lines, but easier to test and document
def by_first(pair):
return pair[0]
sorted_pairs = sorted(pairs, key=by_first)
print(sorted_pairs) # [(1, 'ant'), (2, 'bee'), (3, 'cat')]
Use lambda when the callable is simple (a single expression), used once, and does not need a docstring or a meaningful name in tracebacks. Prefer def when the logic is more than trivial, when you need to test the function independently, or when a clear name would make the calling code easier to read. Assigning a lambda to a variable (like double = lambda x: x * 2) is generally discouraged by PEP 8 — if you are naming it, use def.
def double(x): return x * 2
def apply(func, value): return func(value)
result = apply(double(5), 10)apply(double(5), 10) does not pass double as a function. It evaluates double(5) first, which returns the integer 10. Then apply receives 10 as its func parameter and tries to call 10(10) — but an integer is not callable. Python raises a TypeError, not a clean result.def double(x):
return x * 2
def apply(func, value):
return func(value)
# WRONG — parentheses call double immediately
result = apply(double(5), 10)
# TypeError: 'int' object is not callable
# CORRECT — pass the function object, no parentheses
result = apply(double, 10)
print(result) # 20
double(5) runs immediately and produces 10. The integer 10 is then passed to apply as func. When apply tries to call func(value), it attempts 10(10), which raises TypeError: 'int' object is not callable. To pass the function itself, omit the parentheses: apply(double, 10).def double(x):
return x * 2
def apply(func, value):
return func(value)
# double(5) evaluates to 10 before apply is called
# apply(10, 10) then tries 10(10) — TypeError
try:
apply(double(5), 10)
except TypeError as e:
print(e) # 'int' object is not callable
# Correct: pass the function object without parentheses
print(apply(double, 10)) # 20
apply does not multiply its two arguments together. It calls func(value) — meaning it tries to invoke whatever was passed as func, with value as the argument. Since double(5) evaluates to the integer 10, apply would try to call 10(10), which raises a TypeError. No multiplication happens at the apply level.def double(x):
return x * 2
def apply(func, value):
# apply calls func(value), it does NOT multiply func * value
return func(value)
# double(5) = 10 (an int), so apply receives (10, 10)
# apply then does: 10(10) → TypeError
# The fix is to not call double here:
print(apply(double, 10)) # double(10) = 20
The Callable Protocol
What makes a function object callable is not something magical about the function type. It is a protocol. Any object whose type defines a __call__ method can be called with parentheses. Functions happen to implement this protocol, but so can any class you write:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
triple = Multiplier(3)
# triple is an object, not a function defined with def
print(type(triple)) #
# But it is callable because Multiplier defines __call__
print(callable(triple)) # True
print(triple(10)) # 30
# It can be passed as an argument anywhere a function is expected
numbers = [1, 2, 3, 4, 5]
print(list(map(triple, numbers))) # [3, 6, 9, 12, 15]
The built-in callable() function checks whether an object has this protocol. Under the hood in CPython, calling an object triggers the tp_call slot on the object's type struct — the slot whose signature is PyObject *tp_call(PyObject *callable, PyObject *args, PyObject *kwargs). Since Python 3.8, CPython also supports a faster path called the vectorcall protocol (introduced in PEP 590), which avoids building an intermediate tuple for positional arguments. CPython prefers vectorcall for internal calls when the callable supports it; tp_call remains the general fallback. From the Python programmer's perspective, both paths arrive at the same result: calling the __call__ method on the callable's type. The callable protocol remains unified at the Python level, even as CPython optimizes the C-level dispatch underneath.
When you write MyClass(arg) to create an instance, you are calling the class object. Classes are callable because their metaclass (type) defines __call__. The entire instantiation process -- allocating memory, calling __new__, then __init__ -- is triggered by this same callable protocol.
This means "passable as an argument" is not limited to def functions. It applies to any callable: lambdas, methods, classes, and instances with __call__. They are all objects, and they all implement the callable protocol.
# All of these are callable objects that can be passed as arguments
def func(x):
return x + 1
lam = lambda x: x + 1
class Adder:
def __call__(self, x):
return x + 1
adder_instance = Adder()
for fn in [func, lam, Adder, adder_instance, int, len]:
print(f"{str(fn):.<45} callable={callable(fn)}")
# .................. callable=True
# at 0x...>.............. callable=True
# .................. callable=True
# <__main__.Adder object at 0x...>......... callable=True
# ............................ callable=True
# .................. callable=True
callable(counter) return?class Counter:
def __init__(self): self.n = 0
def __call__(self): self.n += 1; return self.n
counter = Counter()def. Any object whose class defines a __call__ method is callable. Counter defines __call__, so counter (an instance of Counter) is callable. callable(counter) returns True.class Counter:
def __init__(self):
self.n = 0
def __call__(self):
self.n += 1
return self.n
counter = Counter()
print(callable(counter)) # True
print(counter()) # 1
print(counter()) # 2
# counter can be passed as an argument anywhere a callable is expected
nums = [1, 2, 3]
print(list(map(lambda _: counter(), nums))) # [3, 4, 5]
callable() built-in checks whether an object's type defines __call__. Because Counter defines __call__, instances of Counter are callable — callable(counter) returns True. This is exactly the callable protocol in action: the object is not a function defined with def, but it behaves like one and can be passed as an argument anywhere a callable is expected.class Counter:
def __init__(self):
self.n = 0
def __call__(self):
self.n += 1
return self.n
counter = Counter()
print(callable(counter)) # True
print(counter()) # 1
print(counter()) # 2
# Can be used wherever a callable is expected:
def run_twice(fn):
return fn(), fn()
print(run_twice(counter)) # (3, 4)
callable() is a built-in function that works on any Python object — not just def functions. It returns True if the object's type has a __call__ method, and False otherwise. It never raises an AttributeError. For counter, since Counter defines __call__, the result is True.class Counter:
def __init__(self):
self.n = 0
def __call__(self):
self.n += 1
return self.n
counter = Counter()
# callable() works on any object, never raises AttributeError
print(callable(counter)) # True — has __call__
print(callable(42)) # False — integer instances are not callable (42() raises TypeError)
print(callable(print)) # True — built-in function
print(callable("hello")) # False — str is not callable
Higher-Order Functions in Practice
A function that takes another function as an argument, or returns a function as its result, is called a higher-order function. This pattern is the direct consequence of functions being objects. Python's standard library relies on it heavily.
Passing Functions to Built-in Functions
Several of Python's built-in functions accept a callable as an argument and invoke it internally. sorted() takes a key function and applies it to each element before comparing. map() applies a function to every element of an iterable. filter() keeps only the elements for which the function returns a truthy value:
# sorted() accepts a key function
words = ["banana", "apple", "cherry", "date"]
by_length = sorted(words, key=len)
print(by_length) # ['date', 'apple', 'banana', 'cherry']
# map() applies a function to every element
def to_fahrenheit(c):
return c * 9 / 5 + 32
temperatures_c = [0, 20, 37, 100]
temperatures_f = list(map(to_fahrenheit, temperatures_c))
print(temperatures_f) # [32.0, 68.0, 98.6, 212.0]
# filter() keeps elements where the function returns True
def is_even(n):
return n % 2 == 0
evens = list(filter(is_even, range(20)))
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
In each case, the built-in function receives a callable object and invokes it internally. The built-in does not care whether it received a def function, a lambda, a method, or a class with __call__. It calls whatever it was given. The map() function in particular is worth studying as a standalone pattern once you understand the callable protocol.
Writing Your Own Higher-Order Functions
from collections.abc import Callable
import random
from typing import TypeVar
T = TypeVar("T")
def retry(func: Callable[[], T], attempts: int = 3) -> T:
"""Try calling func up to `attempts` times, returning the first success."""
last_error: Exception = RuntimeError("retry called with attempts=0")
for i in range(attempts):
try:
return func()
except Exception as e:
last_error = e
print(f"Attempt {i + 1} failed: {e}")
raise last_error
def unreliable_fetch() -> dict:
if random.random() < 0.7:
raise ConnectionError("Network timeout")
return {"status": "ok", "data": [1, 2, 3]}
# Pass the function object (no parentheses) to retry
result = retry(unreliable_fetch, attempts=5)
print(result)
The retry function knows nothing about what func does. It only knows that func is callable. The retry logic and the business logic are entirely independent, which is only possible because the business logic arrives as a value.
Storing Functions in Data Structures
Because functions are objects, they can be stored in lists, dictionaries, or any other container:
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
operations = {
"+": add,
"-": subtract,
"*": multiply,
}
def calculate(expression):
"""Parse and evaluate a simple 'a op b' expression."""
parts = expression.split()
a, op, b = int(parts[0]), parts[1], int(parts[2])
func = operations.get(op)
if func is None:
raise ValueError(f"Unknown operator: {op}")
return func(a, b)
print(calculate("10 + 5")) # 15
print(calculate("10 - 3")) # 7
print(calculate("10 * 4")) # 40
The dictionary maps operator strings to function objects. Looking up operations["+"] returns the add function object, which is then called with the operands. This dispatch pattern eliminates long chains of if/elif statements and makes it trivial to add new operations by inserting new entries into the dictionary.
Partial Application with functools.partial
Sometimes you have a function that takes several arguments, but you want to pass it somewhere that will only supply some of them. functools.partial solves this by producing a new callable object that wraps the original function with some arguments pre-filled. The result is itself a first-class object and can be passed, stored, or called like any other callable.
import functools
def power(base, exponent):
return base ** exponent
# Create a new callable with exponent pre-filled
square = functools.partial(power, exponent=2)
cube = functools.partial(power, exponent=3)
print(square(5)) # 25 — equivalent to power(5, exponent=2)
print(cube(3)) # 27 — equivalent to power(3, exponent=3)
# The partial object is callable and inspectable
print(type(square)) # <class 'functools.partial'>
print(callable(square)) # True
print(square.func) # <function power at 0x...>
print(square.keywords) # {'exponent': 2}
The most practical application is when an API accepts a single-argument callable but the function you want to use requires more than one argument:
import functools
# sorted() key must be a single-argument callable
words = ["banana", "Apple", "cherry", "date"]
# str.lower takes one argument (the string itself)
# No partial needed here — it already has the right signature
print(sorted(words, key=str.lower))
# ['Apple', 'banana', 'cherry', 'date']
# But suppose we want to sort by the Nth character:
def nth_char(s, n):
return s[n] if n < len(s) else ""
# sorted() needs a one-argument key function.
# Wrap nth_char with n=1 pre-filled:
by_second_char = functools.partial(nth_char, n=1)
print(sorted(words, key=by_second_char))
# ['banana', 'date', 'cherry', 'Apple'] (sorted by index 1: a, a, h, p)
Partial application is conceptually the same as a closure-based factory: both produce a new callable that carries fixed configuration. The difference is that functools.partial is explicit, inspectable, and does not require you to write a wrapper function by hand.
Closures: Functions That Remember
Because functions are objects, they can be created inside other functions and returned as values. When an inner function references variables from its enclosing scope, it captures those variables. The resulting combination of the inner function and its captured variables is called a closure.
def make_greeter(greeting):
"""Return a function that greets with a specific greeting."""
def greeter(name):
return f"{greeting}, {name}!"
return greeter
hello = make_greeter("Hello")
howdy = make_greeter("Howdy")
print(hello("Alice")) # Hello, Alice!
print(howdy("Bob")) # Howdy, Bob!
# The inner function carries its captured variable
print(hello.__closure__)
# (| ,)
print(hello.__closure__[0].cell_contents)
# Hello |
When make_greeter("Hello") returns, its local variable greeting would normally be garbage collected. But the inner greeter function references greeting, so Python keeps it alive inside a cell object stored in the function's __closure__ attribute. The function object carries its environment with it.
Closures are the mechanism behind function factories, a pattern where one function produces customized functions on demand:
def make_validator(min_val, max_val):
"""Return a function that checks if a value is within range."""
def validate(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"{value} is outside [{min_val}, {max_val}]"
)
return value
return validate
validate_percentage = make_validator(0, 100)
validate_temperature = make_validator(-273.15, 1000)
print(validate_percentage(85)) # 85
print(validate_temperature(-40)) # -40
try:
validate_percentage(150)
except ValueError as e:
print(e) # 150 is outside [0, 100]
Each call to make_validator creates a new function object with its own captured min_val and max_val. The returned functions are independent of each other, each carrying its own enclosed state.
hello.__closure__[0].cell_contents return?def make_greeter(greeting):
def greeter(name): return f"{greeting}, {name}!"
return greeter
hello = make_greeter("Hello")greeter references greeting, Python stores it in a cell object inside greeter.__closure__. The variable remains alive as long as the function object (hello) exists. Accessing hello.__closure__[0].cell_contents returns 'Hello'.def make_greeter(greeting):
def greeter(name):
return f"{greeting}, {name}!"
return greeter
hello = make_greeter("Hello")
# make_greeter has returned, but greeting is still alive:
print(hello.__closure__) # (<cell at 0x...>,)
print(hello.__closure__[0].cell_contents) # Hello
print(hello("Alice")) # Hello, Alice!
__closure__ attribute. For functions that do not capture any variables from an enclosing scope, __closure__ is None. For closures — inner functions that do capture enclosing variables — __closure__ is a tuple of cell objects. hello is a closure, so hello.__closure__ is a tuple, and no AttributeError is raised.def make_greeter(greeting):
def greeter(name):
return f"{greeting}, {name}!"
return greeter
hello = make_greeter("Hello")
# All functions have __closure__; it is None if no captures:
def plain(): pass
print(plain.__closure__) # None
# hello captures greeting, so __closure__ is a tuple of cells:
print(hello.__closure__) # (<cell at 0x...>,)
print(hello.__closure__[0].cell_contents) # Hello
greeter is defined inside make_greeter and references greeting, Python wraps that variable in a cell object and stores it in greeter.__closure__. The cell keeps greeting alive even after make_greeter returns. Accessing cell_contents on the first cell gives back the captured value, 'Hello'. This is how closures carry their creation context with them.def make_greeter(greeting):
def greeter(name):
return f"{greeting}, {name}!"
return greeter
hello = make_greeter("Hello")
howdy = make_greeter("Howdy")
# Each closure carries its own captured greeting independently
print(hello.__closure__[0].cell_contents) # Hello
print(howdy.__closure__[0].cell_contents) # Howdy
print(hello("Alice")) # Hello, Alice!
print(howdy("Bob")) # Howdy, Bob!
The Late-Binding Pitfall
Closures capture variable references, not values. This distinction is invisible with simple factories like make_greeter, because the captured variable never changes after the inner function is returned. But it becomes a trap as soon as you create closures inside a loop.
# BROKEN: all three functions capture the same variable i
multipliers = []
for i in range(3):
multipliers.append(lambda x: x * i)
print(multipliers[0](10)) # 20 — expected 0, got 2 * 10
print(multipliers[1](10)) # 20 — expected 10, got 2 * 10
print(multipliers[2](10)) # 20 — correct by accident
# Why? All three lambdas close over the same variable i.
# By the time you call them, the loop has finished and i == 2.
# Each lambda looks up i at call time, not at creation time.
A closure does not snapshot the value of a captured variable at the moment the inner function is created. It stores a reference to the variable's cell. When you call the inner function later, Python resolves the cell to whatever value the variable holds at that moment. If the variable was modified after the closure was created — as a loop variable always is — you will not see the value you expected.
The standard fix is to force early binding by passing the loop variable as a default argument. Default argument values are evaluated at function definition time, so the value is captured immediately:
# FIX 1: default argument captures the current value of i
multipliers = []
for i in range(3):
multipliers.append(lambda x, factor=i: x * factor)
print(multipliers[0](10)) # 0
print(multipliers[1](10)) # 10
print(multipliers[2](10)) # 20
# FIX 2: use functools.partial (see the next section)
import functools
def multiply(x, factor):
return x * factor
multipliers = [functools.partial(multiply, factor=i) for i in range(3)]
print(multipliers[0](10)) # 0
print(multipliers[1](10)) # 10
print(multipliers[2](10)) # 20
Both fixes work for the same reason: they evaluate the loop variable's current value immediately and bind it to something that does not change. The default-argument trick is the more common idiom; functools.partial is discussed in the next section.
Mutating Captured Variables with nonlocal
Closures capture variable references, which means reading a captured variable works naturally. Mutating one does not — at least not without help. If you assign to a name inside an inner function, Python treats that name as a new local variable, shadowing the one from the enclosing scope rather than updating it. The nonlocal keyword fixes this by explicitly declaring that the assignment should reach into the enclosing scope.
def make_counter(start=0):
count = start
def increment():
nonlocal count # without this, count += 1 creates a new local
count += 1
return count
def reset():
nonlocal count
count = start
return increment, reset
inc, rst = make_counter(10)
print(inc()) # 11
print(inc()) # 12
print(inc()) # 13
rst()
print(inc()) # 11 — reset worked
Without nonlocal count, the line count += 1 would raise an UnboundLocalError because Python would see count on the left side of an assignment and classify it as a local variable, then fail to find it in the local scope before the increment. The nonlocal declaration tells Python to look one scope outward instead of creating a fresh local. This pattern produces a stateful callable without needing a class.
nonlocal reaches into the nearest enclosing function scope — it does not reach all the way to the module level. If you want to modify a module-level name from inside a nested function, you still need global. Use nonlocal only when the variable lives inside an outer function. Both keywords exist because Python's default behavior on assignment is to create a new local, and these declarations override that default.
Methods Are First-Class Objects Too
The first-class function model extends to methods. When you access a method through an instance (obj.method), Python does not return the raw function defined on the class. It returns a bound method object — a wrapper that pairs the function with the instance, so that self is automatically supplied on every call. Bound methods are themselves first-class objects.
class Greeter:
def __init__(self, greeting):
self.greeting = greeting
def greet(self, name):
return f"{self.greeting}, {name}!"
g = Greeter("Hello")
# Accessing g.greet produces a bound method
print(type(g.greet)) # <class 'method'>
print(callable(g.greet)) # True
# Assign the bound method to a variable — it carries g with it
fn = g.greet
print(fn("Alice")) # Hello, Alice!
# Pass it as an argument — works exactly like any callable
names = ["Alice", "Bob", "Carol"]
results = list(map(fn, names))
print(results)
# ['Hello, Alice!', 'Hello, Bob!', 'Hello, Carol!']
# Unbound access through the class returns the plain function
print(type(Greeter.greet)) # <class 'function'>
# Must supply self explicitly when calling through the class
print(Greeter.greet(g, "Dave")) # Hello, Dave!
The practical consequence: any instance method can be extracted and passed around as a callable, with no boilerplate. This shows up constantly in real code — passing list.append to a callback, registering self.handle_event with an event system, or passing str.lower as a key function to sorted(). Each of these is a bound method being treated as a first-class value.
From First-Class Functions to Decorators
Decorators are the highest-profile application of first-class functions in Python. A decorator is a function that takes a function as its argument and returns a new function (or the same function, modified). Every piece of this sentence depends on functions being objects.
import functools
import time
def timer(func):
"""Decorator that prints how long a function takes to run."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
print(slow_sum(10_000_000))
# slow_sum took 0.1823s
# 49999995000000
Step by step: Python executes def slow_sum(n): and creates a function object. The @timer syntax passes that function object to timer() as the argument func. Inside timer, a new function object wrapper is created. wrapper is a closure that captures the original func. timer returns wrapper. Python rebinds the name slow_sum to the returned wrapper object.
Every step requires functions to be objects: passing slow_sum as an argument to timer, creating wrapper inside timer, capturing func in the closure, returning wrapper as a value, and assigning it back to the name slow_sum.
Why @functools.wraps Matters
After @timer runs, the name slow_sum points to wrapper, not to the original function. Without @functools.wraps, the function's identity metadata would reflect that replacement:
import functools, time
# WITHOUT functools.wraps
def timer_naive(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"elapsed: {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timer_naive
def slow_sum(n):
"""Return the sum of range(n)."""
return sum(range(n))
print(slow_sum.__name__) # wrapper — WRONG
print(slow_sum.__doc__) # None — WRONG
# WITH functools.wraps
def timer(func):
@functools.wraps(func) # copies __name__, __doc__, __annotations__, etc.
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"elapsed: {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timer
def slow_sum(n):
"""Return the sum of range(n)."""
return sum(range(n))
print(slow_sum.__name__) # slow_sum — correct
print(slow_sum.__doc__) # Return the sum of range(n). — correct
print(slow_sum.__wrapped__) # <function slow_sum at 0x...> — points to original
@functools.wraps(func) copies the original function's __name__, __qualname__, __doc__, __annotations__, and __module__ onto wrapper, and also sets wrapper.__wrapped__ to the original function. This matters for debugging, documentation generators, introspection tools, and for any code that inspects function metadata. It costs nothing at runtime and avoids confusing surprises.
Without the decorator syntax, the same operation looks like this, making the first-class nature even more explicit:
def slow_sum(n):
return sum(range(n))
# This is exactly what @timer does:
slow_sum = timer(slow_sum)
The expression timer(slow_sum) passes a function object as an argument. The expression slow_sum = timer(slow_sum) assigns the returned function object to a variable. Both operations are only possible because functions are objects.
Spot the Bug
Each snippet below contains exactly one bug. Read the code, identify what is wrong, click the option that names the bug, and see the explanation. You can explore all three answer choices.
def square(x): return x ** 2
results = list(map(square(), [1, 2, 3, 4]))map() works perfectly with any iterable as its second argument — including lists, tuples, generators, and custom iterables. The bug is elsewhere: square() is called with parentheses, so Python evaluates square() before map even runs. That immediately raises a TypeError because square requires one argument but got none. The fix is to remove the parentheses: map(square, [1, 2, 3, 4]).square call the function immediately. Python evaluates square() first, but square requires exactly one argument — so Python raises TypeError: square() missing 1 required positional argument: 'x' before map is ever invoked. The fix: pass the function object without parentheses so map can call it on each element.def square(x):
return x ** 2
# BUG: square() called immediately — TypeError before map runs
# results = list(map(square(), [1, 2, 3, 4]))
# FIX: pass the function object without parentheses
results = list(map(square, [1, 2, 3, 4]))
print(results) # [1, 4, 9, 16]
map() call. You can define it anywhere — at module level, inside another function, as a lambda inline, or as a method — and pass the reference. The actual bug is that square() includes parentheses, which calls the function before map can receive it. Remove the parentheses to pass the function object itself: map(square, [1, 2, 3, 4]).def make_counter():
count = 0
def increment():
count += 1
return count
return increment
c = make_counter()
print(c()) # UnboundLocalErrorcount global; it is to add nonlocal count inside increment, which tells Python to reach into the enclosing make_counter scope rather than creating a fresh local.count += 1 and classifies count as a local variable inside increment — because it appears on the left side of an assignment. When the function tries to evaluate the right side (count + 1), it looks for count in the local scope first, finds nothing, and raises UnboundLocalError. The nonlocal keyword tells Python to bind count in the enclosing function's scope instead of creating a new local.def make_counter():
count = 0
def increment():
nonlocal count # ← this line is the fix
count += 1
return count
return increment
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
increment can reference count regardless of where count = 0 appears. The problem is that count += 1 is an assignment, which makes Python treat count as a local variable inside increment. Without nonlocal count, there is no local value to increment, hence UnboundLocalError.import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"took {time.perf_counter()-start:.4f}s")
return result
return wrapper
@timer
def compute(n):
"""Sum up to n."""
return sum(range(n))
print(compute.__name__) # 'wrapper' — not 'compute'return wrapper is correct — a decorator must return a callable (the wrapper function object), not the result of calling it. Writing return wrapper() would immediately invoke the wrapper at decoration time, which is wrong: decorators return a replacement callable that gets invoked later. The actual problem is that @functools.wraps(func) is missing inside wrapper, so the original function's identity metadata is overwritten by the wrapper's.timer is. The mechanism is purely first-class functions: @timer is syntactic sugar for compute = timer(compute). The bug here is the missing @functools.wraps(func), which would preserve the original function's __name__, __doc__, and __annotations__.@timer runs, the name compute points to the wrapper function object. Without @functools.wraps(func), compute.__name__ is 'wrapper', compute.__doc__ is None, and the original docstring is lost. Documentation generators, debuggers, and introspection tools will see the wrapper instead of the original. Always add @functools.wraps(func) as the first decorator on every inner wrapper function.import functools, time
def timer(func):
@functools.wraps(func) # ← this is the fix
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"took {time.perf_counter()-start:.4f}s")
return result
return wrapper
@timer
def compute(n):
"""Sum up to n."""
return sum(range(n))
print(compute.__name__) # compute — correct
print(compute.__doc__) # Sum up to n. — correct
print(compute.__wrapped__) # <function compute at 0x...>
Check Your Understanding
Three questions that connect the concepts across this tutorial. Click an answer to see the feedback, then use try again to explore the other choices.
def and lambda produce objects of exactly the same type: function. You can confirm this with type(lambda x: x). The only practical differences are syntactic: lambda is limited to a single expression, cannot have a docstring, and gets '<lambda>' as its __name__. Neither is more or less a first-class object than the other.def and lambda create function objects. They participate equally in Python's object model: they have an identity, a type, and attributes; they can be assigned to variables, passed as arguments, stored in data structures, and returned from other functions. The only meaningful differences between them are syntactic constraints and the fact that lambda uses '<lambda>' as its __name__.def block creating it has nothing to do with its lifetime after creation. After def greet(name): ... runs, the name greet refers to the function object, not to a return value. You can confirm this by checking type(greet), which returns <class 'function'>. The object persists until the last reference to it is removed.def or lambda statement creates a new function object. The objects are independent (you can verify with is). The issue that makes them all return the same value is late binding: all of the closures reference the same loop variable, and by the time they are called, that variable holds its final value. Fix it with a default argument or functools.partial.lambda x, n=n: ...) or using functools.partial.functools.partial. Calling the factory again would not update existing closures; it would produce new ones.__call__ method. How does this change what you can do with its instances?__call__ on a class makes its instances callable — callable(instance) returns True, and you can invoke the instance with parentheses and arguments. Because the callable protocol is uniform, these instances can be passed to map(), sorted(), higher-order functions, and decorator factories exactly as you would pass a def function. The type is still whatever class you defined, not function, but that does not matter to anything that tests callability.__init__ (and, at a lower level, __new__) — not __call__. You do not need a __call__ method to create instances of a class. What __call__ adds is the ability to invoke an instance using parentheses after it already exists: obj() triggers type(obj).__call__(obj). This makes the instance interchangeable with a function anywhere a callable is expected.__call__ does not change the type of the instance. type(instance) still returns your class. What changes is that the instance now satisfies the callable protocol: Python's internal dispatch mechanism (tp_call) is populated, callable(instance) returns True, and the instance can be invoked with parentheses. The object remains an instance of your class throughout — it is just also now callable.Key Takeaways
- 1
The fundamental rule is that functions are objects. Every
defstatement creates an object of typefunctionwith an identity, a type, and attributes. This is not a special feature — it is a consequence of Python's unified object model where everything is an object. - 2
Referencing a function and calling a function are different operations.
funcgives you the object.func()executes the object and gives you its return value. Passing a function as an argument requires the reference form without parentheses. - 3
lambdacreates anonymous function objects. A lambda expression produces the same type of object asdef, constrained to a single expression. Use it for simple, throwaway callables passed inline; preferdefwhen the logic needs a name, a docstring, or independent testing. - 4
The callable protocol unifies all invocation. Functions, lambdas, methods, classes, and objects with
__call__are all callable through the same mechanism. Thecallable()built-in checks for this capability. - 5
Higher-order functions are the direct consequence. Because functions are values, they can be passed to other functions, returned from functions, and stored in data structures. This enables patterns like callbacks, dispatch tables, function factories, and decorators.
- 6
functools.partialfixes arguments without writing a wrapper. It produces a new callable with one or more arguments pre-filled. Use it instead of a manual closure when the configuration is simple and you want the result to be inspectable. - 7
Closures keep functions connected to their creation context — by reference, not by value. Captured variables are resolved at call time, not at definition time. This causes the late-binding pitfall in loops. Fix it with a default argument or
functools.partialto force immediate evaluation. - 8
Always apply
@functools.wrapsinside decorators. Without it, the wrapper function replaces the original's__name__,__doc__, and__annotations__, breaking introspection, debugging, and documentation tools. - 9
Use
nonlocalto mutate a captured variable. Python treats any name on the left side of an assignment inside a function as a local variable. If you need to increment or reassign a variable from an enclosing scope rather than read it, thenonlocaldeclaration tells Python to bind that name one scope outward instead. - 10
Bound methods are first-class objects. Accessing
obj.methodreturns a bound method that carriesobjas its implicit first argument. You can assign bound methods to variables, store them in containers, and pass them as arguments anywhere a callable is expected — exactly like any other function object.
Everything in Python's higher-order ecosystem — decorators, functools.wraps, functools.partial, map, filter, sorted with key, callback registrations in web frameworks, event-driven architectures — traces back to one decision: def creates an object. Once that rule is internalized, the rest is just applying it.