When is `Callable[..., Digest]` a better factory-parameter annotation than `type[Digest]`?
answer
- Both describe something you call
- One demands a real class object
- Partials and lambdas qualify for only one
- Class identity versus caller flexibility
- The ellipsis waives argument checking
basics
~20 sUse Callable[..., Digest] when callers should be free to supply any producer of a digest — a function, a lambda, a functools.partial. Use type[Digest] when the value must genuinely be a class object you construct or inspect.
solid answer
~40 s`type[Digest]` demands a real class object: it admits only `Digest` subclasses, refuses abstract ones, checks your `cls()` call against the constructor, and lets you read class-level members such as `__name__` or run `issubclass`. `Callable[..., Digest]` demands only *something callable that returns a digest*, so a plain function, a lambda, a `functools.partial` with arguments pre-bound, or an instance with `__call__` all qualify — but you get no class identity back, so `issubclass` and `__name__` are gone. The `...` also waives argument checking entirely; `Callable[[], Digest]` is stricter and worth preferring when the factory really is called with no arguments. Choose by what the receiving code does: construct-and-inspect wants the class, construct-only wants the callable, and the callable form is the more permissive extension point.
code
python · 11 linesfrom collections.abc import Callable
from functools import partial
class Digest:
def __init__(self, subject: str) -> None:
self.subject = subject
def run(factory: Callable[[], Digest]) -> Digest:
return factory()
print(run(partial(Digest, "Weekly recap")).subject)go deeper
Know that both annotations describe something you call to get an object, and that only the class-object form guarantees you were actually handed a class rather than a function or a lambda.
Explain the trade concretely: constructor checking, class identity and the abstractness guarantee on one side; functions, lambdas and pre-bound partials on the other. Also know what the ellipsis waives.
Show the operational consequence of choosing wrong — logging or metrics keyed on a name the callable does not have — and pick the annotation from what the body actually does with the value.
Own how open the extension point should be. A class-object contract makes implementers subclass and keeps identity available; a callable contract admits adapters and pre-bound configuration but pushes naming and identity into explicit parameters.
## Two annotations for what looks like the same slot Both of these describe *a thing I will call to get a digest*: ```python def make(source: type[Digest]) -> Digest: ... def make(source: Callable[..., Digest]) -> Digest: ... ``` They differ in what callers may hand over and in what the body is allowed to do afterwards, and the choice is a small API design decision that shows up in every plugin registry. ## What `type[Digest]` gives and demands It demands an honest class object in the `Digest` hierarchy. In exchange the body gets: - **Constructor checking.** `cls(subject)` is validated against the `__init__` the checker can see on `Digest`. - **Class identity.** `cls.__name__` for logging, `issubclass(cls, X)` for dispatch, class-level attributes and `ClassVar` declarations, and the class itself as a stable dict key. - **A constructibility guarantee**, which is why an abstract base is refused in that position. And it excludes everything that is not a class: a factory function, a `functools.partial` with the subject pre-bound, a bound method, a callable instance. If a caller wants to register `partial(HtmlDigest, subject="Weekly recap")`, `type[Digest]` simply will not accept it. ## What `Callable[..., Digest]` gives and demands It demands only callability and a return type. That makes it the more **permissive** extension point — the widest set of things a plugin author can register — at the cost of everything class-shaped: - No `__name__` you can rely on. A `functools.partial` object does not have one, and a lambda's is uninformative. - No `issubclass`, no class-keyed registry entry, no class attributes. - No abstractness check, because the abstract question does not arise: you are calling a callable, not instantiating a class. The ellipsis deserves attention of its own. `Callable[..., Digest]` waives argument checking completely — any call arity type-checks. If the receiving code always calls with no arguments, `Callable[[], Digest]` says so and catches the caller who registers something needing a subject. Reach for `...` only when the arity genuinely varies, and note that a class object satisfies a `Callable` annotation too, so the callable form is the strictly wider of the pair. ## Choosing between them Ask what the body actually does. **Constructs and inspects** — logs the class name, keys a cache on the class, checks ancestry — then the class object is the data you need, and `type[Digest]` is right. Narrowing to the class also documents the extension point: implementers subclass, they do not hand over closures. **Only constructs** — calls it once and forgets where it came from — then `Callable` is the better fit, and it costs the caller nothing to pass a class, since classes are callable. This is the annotation to choose when you want to let callers pre-bind configuration, adapt a legacy constructor, or supply a test double without inventing a subclass. **Needs both** — a registry that constructs *and* reports which renderer ran — then either take the class, or take a callable plus an explicit name parameter. Do not take a callable and then reach for `__name__` on it; that is the version of this bug that survives review and breaks the first time somebody registers a partial. ## The abstract-class asymmetry There is a neat consequence worth holding on to. Passing an abstract class as `type[Digest]` is rejected statically, because the annotation promises constructibility. Passing that same abstract class where a `Callable[..., Digest]` is expected is *not* obviously wrong to a checker in the same way, because the class is callable in the general sense — and the call still fails at runtime with `TypeError`. Widening the annotation to `Callable` therefore trades a static guarantee for flexibility. That is a fine trade when you need the flexibility, and a bad one when you widened the annotation merely to silence an error you should have fixed. ## A note on spelling Prefer `collections.abc.Callable` in annotations; it has been subscriptable since PEP 585 in Python 3.9. `typing.Callable` still works on 3.14 and means the same thing, but it is the deprecated alias, the same relationship `typing.Type` has to the builtin `type`.
- Why prefer `Callable[[], Digest]` over `Callable[..., Digest]`?The ellipsis waives argument checking, so any call arity is accepted and a caller who registers a factory needing a subject sails through the checker and fails at runtime. If the receiving code always calls with no arguments, spelling that as an empty parameter list catches the mismatch where it belongs. Use `...` only when the arity genuinely varies.
- Does a class object satisfy a `Callable[..., Digest]` annotation?Yes — classes are callable, and calling one returns an instance. That makes the callable form strictly wider than `type[Digest]`: every class the narrow annotation accepts is also accepted by the callable one, plus functions, lambdas, partials and callable instances. The narrowing is what buys you class identity and the constructibility guarantee, not the other way round.
- What goes wrong if you widen to `Callable` but still want the renderer's name?A `functools.partial` has no useful name, and a lambda's is uninformative, so any logging or metrics keyed on the callable's name degrades the moment somebody registers one. Either keep the class-object annotation, or take an explicit name alongside the callable so the identity is data the caller supplies rather than something you scrape off the object.
saying these in an interview costs you the question
- Thinks the two annotations are interchangeable
- Reads a name off a partial passed as a callable
- Uses the ellipsis when the arity is fixed
- Widens to Callable to silence an abstract-class error
- Claims a class object fails a Callable annotation
- Expects issubclass to work on a callable parameter