Python APIs often ask for "a function": a sort key, callback, validator, route handler, retry policy, or dependency provider. Usually the real requirement is narrower and more powerful: an object that can be called with a particular argument shape.
Functions satisfy that protocol, but so do bound methods, classes, built-ins, functools.partial objects, and instances defining __call__. They do not share all function attributes or binding behavior. Designing against callability means depending on invocation while staying precise about signatures, state, identity, and introspection.
Version note. Calling an object through
__call__and function descriptor binding are Python language behavior. Examples target Python 3.10 through 3.14 and were verified on CPython 3.14. Vectorcall, adaptive call bytecode, object layouts, and exact timings are CPython implementation details.
Experiment 1: an instance can own callable state
class AtLeast:
def __init__(self, minimum):
self.minimum = minimum
self.calls = 0
def __call__(self, value):
self.calls += 1
return value >= self.minimum
adult = AtLeast(18)
print("[check] adult validator is callable:", callable(adult))
print("[result] ages accepted by minimum age 18:", [age for age in [12, 18, 30] if adult(age)])
print("[state] validator call count after filtering three ages:", adult.calls)
Call syntax evaluates the target and arguments, then invokes the target's call behavior. Defining __call__ on a class makes its instances callable. The instance can keep configuration and evolving state without globals or an external dictionary keyed by function identity.
This pattern works well for configurable policies, test fakes that count calls, incremental parsers, memoizers with explicit controls, and adapters implementing an interface. It is less suitable when a plain function communicates the whole behavior. A class with only a ceremonial constructor and stateless __call__ adds vocabulary without capability.
callable(obj) asks whether the object appears callable. A true result does not prove a particular signature or guarantee that a call succeeds; classes can construct with required arguments, and a __call__ implementation can raise. Type annotations and tests describe the expected call shape.
Special methods are looked up on the type, not normally through instance attributes. Assigning instance.__call__ = something does not reliably redefine instance() because implicit special-method lookup bypasses the instance dictionary. Change the class or wrap the object instead.
Experiment 2: functions, closures, and callable instances retain state differently
def make_multiplier(factor):
calls = 0
def multiply(value):
nonlocal calls
calls += 1
return value * factor, calls
return multiply
times_three = make_multiplier(3)
print("[result] times_three(4), with call count:", times_three(4))
print("[result] times_three(5), with call count:", times_three(5))
print("[state] closure function name:", times_three.__name__)
A closure stores captured variables in cells associated with a function. A callable instance stores state in attributes. Both can implement the same invocation. Choose based on the surrounding operations.
A closure is compact when callers only invoke it and captured state should remain private. A callable class is clearer when state deserves names, inspection, reset methods, serialization, subclassing, or several related operations. A class also makes many independently configured instances unsurprising. Neither choice is intrinsically faster or more Pythonic.
Plain functions remain objects. They have metadata such as __name__, __qualname__, __annotations__, and usually __dict__; user code may attach attributes. That possibility does not make function attributes an ideal general state store. It hides mutation on an object readers expect to describe behavior. Closures and classes state ownership more directly.
CPython represents closure cells and callable instances differently, but code should not rely on size comparisons from one build. Measure complete workloads if allocation or call overhead matters. Usually I/O and application work dominate the small dispatch difference.
Experiment 3: partial application freezes arguments
from functools import partial
def record(level, message, *, service):
return f"[{level}] {service}: {message}"
audit = partial(record, "INFO", service="accounts")
print("[result] audit log message:", audit("login accepted"))
print("[check] partial wraps the record function:", audit.func is record)
print("[state] partial positional arguments:", audit.args)
print("[state] partial keyword arguments:", audit.keywords)
partial creates a callable that supplies some positional and keyword arguments before forwarding new ones. It is ideal when the behavior already has a good name and only configuration differs. It avoids a wrapper whose body merely rearranges arguments.
Partials expose .func, .args, and .keywords, which can improve debugging. They are callable objects, not ordinary function objects: do not assume every callable has __name__ or __code__. inspect.signature understands many standard callable forms, but introspection can fail or report implementation-defined signatures for some extension objects.
Python 3.14 added functools.Placeholder, allowing holes among positional arguments rather than freezing only a prefix. Code supporting older versions cannot use that feature. This tutorial's compatibility baseline therefore uses traditional leading argument binding. Version-specific conveniences should not silently leak into a package claiming an earlier minimum.
Prefer a named wrapper when argument adaptation includes validation, exception translation, observability, or domain meaning worth documenting. Prefer partial when it is genuinely argument binding.
Experiment 4: functions bind as methods because they are descriptors
class Greeter:
def greet(self, name):
return f"hello, {name}"
greeter = Greeter()
bound = greeter.greet
print("[result] bound method greeting for Ada:", bound("Ada"))
print("[check] bound method stores the greeter instance:", bound.__self__ is greeter)
print("[check] bound method stores Greeter.greet:", bound.__func__ is Greeter.greet)
print("[result] unbound Greeter.greet call for Grace:", Greeter.greet(greeter, "Grace"))
A function stored on a class implements the descriptor protocol. Access through an instance produces a bound method carrying the instance in __self__ and the original function in __func__. Calling that method supplies the instance before explicit arguments.
This is why self is a conventionally named parameter rather than a keyword. Binding supplies the first argument. A callable object assigned as a class attribute does not automatically gain identical behavior unless its type implements __get__. "Callable" and "method descriptor" are separate protocols.
staticmethod suppresses instance binding and returns the underlying callable without adding an instance. classmethod binds the class. Use them for semantic ownership, not to save an allocation. Ordinary instance methods are right when behavior depends on instance state; class methods often name alternate constructors; static methods are useful only when class namespacing adds genuine meaning.
Bound method objects may be newly produced on each attribute access, so identity comparisons such as obj.method is obj.method are not a portable registration strategy. Store the exact callback token returned by a registration API or compare the documented pair of instance and function where appropriate.
Experiment 5: decorators must preserve the callable's public face
from functools import wraps
from inspect import signature
def traced(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"[event] calling decorated function {function.__name__}")
return function(*args, **kwargs)
return wrapper
@traced
def total(left: int, right: int = 0) -> int:
"""Add two values."""
return left + right
print("[result] decorated total for left=3 and right=4:", total(3, right=4))
print("[state] decorated function name and docstring:", total.__name__, total.__doc__)
print("[state] preserved decorated function signature:", signature(total))
A decorator executes at function definition time and replaces the bound name with its result. The replacement need only be callable, but frameworks often inspect names, annotations, signatures, docstrings, and the __wrapped__ chain. functools.wraps copies conventional metadata and sets __wrapped__, allowing inspect.signature and tools to reach the original.
Metadata preservation is not signature enforcement. The wrapper still accepts *args, **kwargs at runtime, then the wrapped function validates binding. If a framework needs the wrapper itself to expose a generated signature or if static typing must preserve parameter structure, use ParamSpec in annotations and understand the framework's introspection rules.
Decorator state can live in closure cells or in a callable wrapper instance. Class-based decorators need extra care around methods: a wrapper object stored on a class must implement descriptor binding or use a function wrapper, otherwise method calls may omit the instance. This is a common example of accidentally implementing callability while losing function-like behavior.
Experiment 6: signatures are contracts, not just argument counts
from inspect import signature
def dispatch(handler, payload):
expected = signature(handler)
expected.bind(payload, source="queue")
return handler(payload, source="queue")
class Handler:
def __call__(self, payload, *, source):
return f"{source}: {payload}"
handler = Handler()
print("[state] callable handler signature:", signature(handler))
print("[result] dispatched payload 'job-17':", dispatch(handler, "job-17"))
The shape includes positional-only, positional-or-keyword, keyword-only, variadic, and defaulted parameters. inspect.Signature.bind applies Python's binding rules without running the callable, useful for adapters and diagnostics. It still cannot prove semantic compatibility or successful execution.
Avoid routinely inspecting signatures to decide how to call user callbacks. It can fail for opaque built-ins, wrappers can publish custom signatures, and branch-by-introspection creates ambiguous APIs. Define one callback contract and call it consistently. Use binding checks at registration time only when early errors materially improve the interface.
Static typing expresses callable shapes with collections.abc.Callable or callback protocols. Callable[[bytes], str] handles straightforward positional signatures. A protocol with __call__ can express named and keyword-only parameters plus additional attributes. Type checking is not runtime dispatch; Python still calls the object normally.
What CPython optimizes
Modern CPython uses the vectorcall protocol internally for many callable types. It can pass positional and keyword arguments in a compact layout and avoid creating temporary tuples and dictionaries in common calls. The specializing interpreter can also optimize observed call patterns. These mechanisms help explain why microbenchmarks change across releases.
Vectorcall is not a Python-level contract for ordinary application classes. Do not redesign a callback API around a private expectation that one callable kind is cheaper. Extension authors have a documented C API, with version-specific stability rules, but Python applications should optimize larger boundaries first: avoid needless calls in hot loops, batch work, and profile representative execution.
Argument evaluation order is language-visible. The callable expression and argument expressions are evaluated before invocation, with documented ordering and duplicate-key checks. Side effects in argument construction happen even if the call later fails binding. Keep argument expressions unsurprising and do validation inside appropriately named boundaries.
Design guidance for callable APIs
Name the expected invocation and lifetime. Is the callback called once, concurrently, during retries, after its owner closes, or from another thread? Can it mutate input? Are exceptions propagated, translated, or logged? These questions matter more than whether the provider uses def.
Do not demand function-only attributes unless they are truly required. If an API accepts any callable, error messages and logging should use safe fallbacks such as getattr(handler, "__qualname__", type(handler).__qualname__). If pickling or weak references are requirements, state and test them separately; callability does not imply either.
Beware hidden retention. A bound method strongly references its instance. A closure references captured cells. A partial references its function and frozen arguments. A callback registry can therefore keep an entire service graph alive. Explicit unregister operations, weak-reference designs, or lifecycle-scoped registries solve that ownership issue.
Callable equality and identity also affect registries. Two closures made by the same factory are distinct objects. Repeated access to a method can produce distinct bound method objects even though they refer to the same receiver and function. A callable class may define value equality, making two configured policies compare equal while still carrying separate counters. Registration APIs should return an opaque removal token rather than forcing callers to reconstruct callable identity.
Exceptions are another part of the callback contract. Decide whether one failing callback stops dispatch, whether remaining callbacks run, and whether results preserve input order. Catching Exception around every callback can keep a dispatcher alive while silently losing required work. Let failures propagate by default, or aggregate them through a documented mechanism when independent handlers genuinely should all run.
For async APIs, "callable" is insufficiently precise. Calling an async function returns an awaitable; it does not perform the body to completion. A callback contract must say whether providers are synchronous, return awaitables, or may be either. Supporting both often adds subtle exception and cancellation paths. Separate registration methods can produce a clearer boundary than dynamically testing every return value.
Exercises: choose the right callable
- Implement one configurable predicate as a closure, partial, and callable class. Compare repr, reset behavior, and inspectable state.
- Write a decorator without
wraps, inspect its signature and metadata, then repair it. - Build a callback protocol requiring a keyword-only
sourceargument and annotatedispatchwith it. - Register a bound method in a list, delete the original instance name, and use
weakrefto observe retention. - Assign a plain callable instance to a class and compare access with a function method. Explain the missing descriptor behavior.
- Use
Signature.bindto validate several correct and incorrect calls without invoking their targets.
Keep this model
Callability is a protocol, not a synonym for function. Functions bring metadata and descriptor binding. Closures capture cells. Partials bind arguments. Bound methods carry a receiver. Callable instances make configuration and mutable state explicit.
Depend on the smallest behavior your API needs, but document the full invocation and lifetime contract. Preserve metadata when wrapping, do not assume function-only attributes, and treat CPython call optimizations as measurements rather than semantics.