Why can a Callable[[Animal], None] be passed where Callable[[Dog], None] is expected?
answer
- Read the slot as a promise
- The callback must survive what it is handed
- Arguments and returns point opposite ways
- Consumers widen, producers narrow
- Parameters contravariant, return covariant
basics
~20 sBecause a callable's parameter positions are contravariant. The slot promises the callback will only ever be handed a Dog, and a function that handles any Animal handles that. The reverse direction is the unsafe one.
solid answer
~40 sFor `collections.abc.Callable`, the argument types are **contravariant** and the return type is **covariant**. Read the expected type as a promise about the call site: a slot typed `Callable[[Dog], None]` guarantees the callback will be invoked with a `Dog` and nothing else. A function declared `def handle(a: Animal) -> None` accepts every `Dog`, so it satisfies that promise — it is simply more capable than required. The reverse fails: a `Callable[[Dog], None]` cannot fill a `Callable[[Animal], None]` slot, because that slot may hand it a `Cat`. Returns run the other way — a function returning `Dog` fits a slot expecting a callable returning `Animal`, since the caller only reads the result. The single rule underneath both is substitutability: accept more, return less.
code
python · 12 linesfrom collections.abc import Callable
class Event: ...
class BidEvent(Event): ...
def audit(e: Event) -> None:
print("audited", type(e).__name__)
def on_bid(handler: Callable[[BidEvent], None]) -> None:
handler(BidEvent())
on_bid(audit) # accepted: parameters are contravariantgo deeper
Recall the direction: a callback that accepts a broader type can be used where a narrower one is expected. You are not expected to derive it, only to recognise which of the two assignments a checker allows.
Explain it by reading the expected type as a promise about how the callback will be invoked. Then state the pairing plainly: argument positions are contravariant, the return position is covariant.
Show the production angle. Diagnose why a registration was rejected, argue against papering over it with an ellipsis parameter list, and connect the static error to the runtime AttributeError it prevents inside somebody else's callback.
Own the callback contract across a system. Decide how event handlers are typed so broad consumers compose, when a Protocol with call earns its keep over a plain callable type, and what the team is allowed to erase with an ellipsis.
### The rule Given a callable type written `Callable[[P], R]`, the parameter type `P` is **contravariant** and the return type `R` is **covariant**. Spelled out for a hierarchy where `Dog` subclasses `Animal`: - `Callable[[Animal], None]` **is** assignable to `Callable[[Dog], None]` — broader parameter, accepted. - `Callable[[Dog], None]` **is not** assignable to `Callable[[Animal], None]` — narrower parameter, rejected. - `Callable[[str], Dog]` **is** assignable to `Callable[[str], Animal]` — narrower return, accepted. - `Callable[[str], Animal]` **is not** assignable to `Callable[[str], Dog]`. This surprises people because it points the opposite way to the container rule they just learned. The trick is to stop thinking about the callback and start thinking about the *slot*. ### Read the annotation as a promise A parameter annotated `Callable[[Dog], None]` is a statement made by the function that will *do the calling*: "whatever you give me, I will invoke it with exactly one `Dog` argument and ignore its return value." Anything able to survive that treatment is a valid argument. A function that accepts any `Animal` survives it trivially — a `Dog` is an `Animal`. A function that accepts only `Dog` also survives it, and one that accepts `object` survives it best of all. What does *not* survive is a narrower requirement: a callback declared `def only_cats(c: Cat) -> None` cannot be registered, because the slot will hand it a `Dog`. Now flip it. A slot typed `Callable[[Animal], None]` promises to call the callback with *some* animal — possibly a `Cat`. A `Dog`-only function cannot fulfil that, so it is rejected. Contravariance is not an exotic rule; it is exactly what "the callback must handle everything the caller might pass" means, written down. ### A concrete setting An ad-auction bidder registers per-event callbacks. The registry declares ```python def on_bid(handler: Callable[[BidEvent], None]) -> None: ... ``` A team writes a general audit handler typed `def audit(e: Event) -> None`, where `BidEvent` subclasses `Event`. That handler registers cleanly on `on_bid`, and on every other event hook in the system, because contravariance lets one broad consumer fill many narrow slots. The reverse attempt — registering a `Callable[[BidEvent], None]` on a generic `on_event(handler: Callable[[Event], None])` hook — is correctly rejected, and it is rejected for a reason worth stating in review: that handler would eventually be invoked with a click event and would fail on the first attribute it read. ### The same rule, from the producer/consumer angle A type parameter may be covariant when it only appears in output positions and contravariant when it only appears in input positions. A callable object *consumes* its arguments and *produces* its return value, so parameters are the input positions and the return is the output position. That is the whole derivation. It is also why a mutable container, which both produces (`__getitem__`) and consumes (`append`) its element type, is stuck at invariance. ### Practical consequences - **Widen your callback parameters deliberately.** A handler typed to take `object` or a broad base class is registrable almost anywhere. That is a feature when writing generic logging or metrics hooks, and a liability when it hides a real assumption about what it receives. - **Beware the escape hatch.** Writing `Callable[..., None]` erases the parameter list entirely, so nothing is checked about arity or argument types. It is occasionally necessary, but it silently opts out of exactly the check this question is about. - **A Protocol with `__call__` follows the same rule.** Defining a callback interface as a class with a `__call__` method gives you named parameters and default arguments, and its parameter positions are still contravariant. - **Keyword names matter.** A callable type written with a positional parameter list makes no promise about parameter *names*, so a checker will not let the caller invoke it by keyword. If callers must use keywords, express the callback as a `Protocol` with `__call__`. ### Runtime reality As with every variance rule, none of this is enforced by the interpreter. Registering a `Cat`-only handler on a `Dog` hook is accepted at runtime and fails later, inside the callback, with an `AttributeError` on the first `Cat`-specific attribute. The traceback points at the callback body, not at the registration line where the mistake was actually made — which is precisely why catching it statically is worth more here than in most places. ### Version notes Use `collections.abc.Callable` in annotations; it has been subscriptable since Python 3.9 (PEP 585) and the `typing.Callable` alias is deprecated. On Python 3.10 and later the shorthand `Animal | None` may appear in these signatures. The contravariance of parameter positions is unchanged on 3.14.
- Which way does the return type of a callable vary?Covariantly. A function returning `Dog` satisfies a slot typed `Callable[[str], Animal]`, because the caller only reads the result and a `Dog` is an `Animal`. The reverse is rejected: a function returning `Animal` cannot fill a slot promising a `Dog`, since the caller may immediately use a dog-only attribute.
- What does writing Callable[..., None] give up?All parameter checking. The ellipsis form says nothing about arity or argument types, so a callback with the wrong number of parameters, or one that cannot handle what the caller passes, is accepted silently. Use it only where the signature genuinely varies, and prefer a `Protocol` with `__call__` when you need named or optional parameters checked.
- Why does this rule feel backwards after learning that list is invariant?They come from the same principle applied to different positions. A parameter is an input position, so it varies contravariantly; a return is an output position, so it varies covariantly. A mutable container has its element type in both positions at once, which is why neither relaxation is sound for it and it lands on invariance.
A job ad asking for someone who can drive a van is satisfied by a candidate licensed for every vehicle on the road; it is not satisfied by someone licensed only for motorbikes.
saying these in an interview costs you the question
- Assumes a Dog-only callback fits an Animal callback slot
- Says callables are covariant in their arguments like containers
- Reaches for Callable[..., None] to make the error go away
- Cannot state which position varies which way
- Thinks a callable type also constrains parameter names
- Believes the interpreter checks callback signatures at registration