Functions in Python are first-class values. They can be passed as arguments, stored in dicts, and returned from other functions. The typing module's Callable describes the shape of a function-typed value so a type checker can verify what gets called and what comes back. This lesson covers every flavor of Callable, the ParamSpec machinery that makes decorators type-safe, and the Protocol alternative for callables with structure beyond a simple signature.
The annotation form is Callable[[arg_types], return_type]. The argument types live in a bracketed list (even if there's only one), and the return type follows after the inner bracket closes. The whole thing reads "a callable that takes these arguments and returns this".
The rule parameter is any callable that takes a float and returns a float. The three calls pass three different shapes of callable: two named functions and one lambda. All three match the signature, so the type checker accepts every call site.
A quick reference for the common shapes:
| Annotation | What it accepts |
|---|---|
Callable[[], int] | No arguments, returns int |
Callable[[float], float] | One float, returns float |
Callable[[str, int], bool] | Two arguments, returns bool |
Callable[[str], None] | One argument, returns nothing useful |
Callable[..., int] | Any arguments, returns int |
Callable[..., Any] | Any arguments, any return (use sparingly) |
The empty inner bracket in Callable[[], int] is required. Writing Callable[int] is a syntax error in this position, because the first slot is always the argument list.
One subtle point: the annotation describes the call shape, not whether the value is literally a function. A class with a __call__ method, a bound method, a functools.partial object, even a lambda, all satisfy Callable[...] if their call signature matches. The __call__ and Protocol sections later in the lesson cover this.
A higher-order function is one that takes a function as an argument or returns one. Python's standard library is full of them, and writing one is the most common reason to use Callable.
A small order-pricing utility lets the caller plug in a custom rounding rule:
The function describes the rule's shape (float -> float) once. Every plug-in rule has to match, and the checker enforces that match at the call site. A rule with the wrong signature surfaces an error in the editor before the test suite runs.
The same idea scales to filtering and mapping. Built-ins like filter and map are themselves higher-order, and their typed signatures look like this conceptually:
keep_in_stock takes any predicate that maps a product dict to a bool. transform_prices takes a float -> float rule. The lambdas at the call site fit the declared shapes, so the checker accepts both.
Calling a Python function has real overhead, more than an inline expression. For a list of ten or even a thousand items this is irrelevant. Inside tight loops that run millions of times, the per-call cost of predicate(item) can dominate. Profile before refactoring.
Sometimes the signature is irrelevant. The only requirement is "this value is callable". Two forms cover that case, and they say slightly different things.
Callable on its own (with no subscript) means "anything that can be called". The checker treats it as a callable with an unknown signature, which is roughly the same as Callable[..., Any]. All type information about arguments and return value is lost.
Bare Callable is fine when the callable's behavior is genuinely unknown or irrelevant. It appears in plugin registries, generic event dispatchers, and decorator scaffolding.
Callable[..., R] (with the literal ellipsis ...) is a small step up. It says "any arguments, but the return type is known". The return-type check is preserved but the argument check is waived.
The two callables have different argument shapes, but both return int. Callable[..., int] accepts either. The checker still verifies that whatever log_result returns is an int.
When are these escape hatches appropriate?
| Intent | Form | Cost |
|---|---|---|
| Anything callable | Callable | No argument or return type checking |
| Any signature, known return | Callable[..., R] | No argument checking |
| Exact signature | Callable[[A, B], R] | Full checking |
| Decorator-style passthrough | ParamSpec | Full checking, generic over args |
The rule of thumb: be as specific as possible. The whole reason to write a type annotation is to give the checker something to check. Callable[..., Any] is usually a sign that the function's contract with the callable could be tighter.
Callbacks are a common use case for Callable annotations. Anywhere code says "call me back when X happens", a Callable annotation describes the expected handler shape.
A small order-event dispatcher makes the pattern concrete:
Two design choices to call out. First, the OrderHandler type alias makes the signature reusable. Three different parts of the file (the field, the parameter, and the registered functions) all share one name, so a change in one place ripples cleanly. Second, the dispatcher's _handlers dict is typed dict[str, list[OrderHandler]], which tells the checker that every value in the list has the same call shape.
A common variation is a handler that returns a value the caller can act on, like a middleware-style filter:
Each filter is a Callable[[dict[str, object]], dict[str, object] | None]. Returning None means "stop the chain". The annotation captures both the success and the short-circuit cases in one line.
The diagram shows the fan-out shape. emit looks up registered handlers for the event name and calls each one in turn. Every handler shares the same Callable[[int, str], None] signature, which allows the dispatcher to store them in one list and call them uniformly.
A function that returns another function is a function factory. The return annotation is a Callable[...] describing the shape of the returned function.
The factory's return type is Callable[[float], float]. Callers know exactly what comes back without reading the body. The closure captures rate or amount from the enclosing scope, and the returned function is an ordinary float -> float callable.
Factories build small, reusable strategies without dragging a class hierarchy along. A common pattern is a registry of factories keyed by name:
The DiscountFactory alias is a callable that returns a callable. Reading the annotation left to right: "given a float, return a callable that maps a float to a float." The nested shape is verbose, but it's accurate, and the alias keeps the rest of the code readable.
A decorator is a function that takes a function and returns one. The hard part of typing decorators is preserving the wrapped function's signature so callers still see the right argument types and return type.
The naive approach uses Callable[..., Any] and loses all type information:
This works at runtime, but a type checker now thinks order_total accepts any arguments and returns Any. A caller who writes order_total("nope") gets no static error. The annotation has erased the very information that made type hints useful.
ParamSpec, added in Python 3.10 (PEP 612), fixes this. It's a special kind of type variable that captures a callable's argument signature as one unit, so the wrapper can replay it.
P is the ParamSpec. P.args is the tuple of positional arguments and P.kwargs is the dict of keyword arguments. R is a regular TypeVar that captures the return type. The decorator's signature now says: "take a callable with some parameter spec P returning R, and return a callable with the same P returning the same R."
After applying @log_calls, order_total keeps its (float, int) -> float signature. A call like order_total("nope") triggers a static type error, exactly as it would without the decorator. The wrapper inherits the wrapped function's argument types and return type instead of erasing them.
ParamSpec is well-suited to wrappers that change the return type but preserve the arguments. A caching decorator that wraps the result in a small dataclass, for instance:
The wrapped shipping_quote is now Callable[[float, str], Cached]. The argument signature is preserved exactly, and the return type changes from float to Cached. The checker still rejects bad call sites like shipping_quote("nope").
ParamSpec requires Python 3.10 or later. For older versions, the typing_extensions package backports it. A project targeting 3.9 should install typing_extensions and import from there instead.
Any object with a __call__ method is callable. The instance can be passed anywhere a Callable is expected, and the type checker treats it like a function.
PercentDiscount instances are callables. The __call__ method has the signature (self, price: float) -> float, and the checker treats an instance as Callable[[float], float] (the implicit self is bound and drops out of the public shape). apply_to_cart accepts either a function, a lambda, or a PercentDiscount instance interchangeably.
Why use a class instead of a plain function? State. A class instance can hold configuration, accumulate counts, lazy-load resources, or expose introspection methods. A closure-based factory does the same job for simple cases; a class is preferable when methods like reset(), __repr__(), or inspectable attributes are required.
The first two calls apply the discount and decrement the remaining count. The third call has no uses left, so the price passes through unchanged. The remaining() method exposes the state for the caller to read. None of this is possible with a plain function without resorting to global state or mutable defaults, both of which are worse choices.
The instance still satisfies Callable[[float], float] for any function that takes one, so the same apply_to_cart from earlier accepts it without changes.
Callable describes a single signature. For a callable that also has attributes, or one whose signature needs a name for documentation, or one with overloaded forms, Protocol with a __call__ method covers the gap.
A Protocol is a structural type: anything with the right shape satisfies it, no inheritance required. When the protocol declares a __call__, instances of any class that defines a matching __call__ qualify as members.
DiscountRule is a protocol that says "must be callable as (price: float) -> float and must have a name attribute". Neither PercentDiscount nor FlatDiscount inherits from DiscountRule, but both satisfy the protocol structurally. describe_and_apply can rely on both the call and the attribute.
A plain Callable[[float], float] annotation can't express the name requirement. That's the niche Protocol fills: callable shape plus structure.
The diagram shows the structural-typing relationship. Neither concrete class inherits from DiscountRule. Both have the right shape, so both qualify automatically. The function that uses the protocol is indifferent to which class produced the value.
Protocols also work for a callable with keyword-only parameters or overloads, both of which are hard to express in a single Callable[...] annotation:
Callable[[str], list[str]] can't say "and there's a keyword-only limit with a default of 10". A Protocol with a __call__ method can, because protocol bodies are normal function declarations and accept every parameter form Python supports.
The trade-off is verbosity. For a simple (float) -> float rule, Callable[[float], float] is shorter and clearer. Use a Protocol when the signature has structure that doesn't fit the Callable[[args], return] mold, or when the callable also needs attributes.
A few Callable-shaped patterns come up often.
Strategy registry. A dict that maps a string key to a callable. The dispatch function selects the strategy at runtime based on a tag.
The PaymentHandler alias gives the registry a clear, named type. Adding a new payment method means writing one function and adding one entry to the dict.
Late-bound callback. A function that stores a callback and runs it later when some condition is met.
Each callback is a Callable[[], None]. The class stores any number of them and fires them in order when ready() is called.
Partial application. functools.partial builds a new callable with some arguments pre-filled. The result is still callable, and the type checker treats it as one if you annotate it explicitly.
The annotation on us_standard tells the checker what the result looks like after partial application. Without that annotation, partial returns a functools.partial object whose call shape isn't always inferrable to the checker.
10 quizzes