In Python typing, how does a parameter annotated `type[Digest]` differ from one annotated `Digest`?
answer
- Classes are values too
- Cookie cutter, not the cookie
- Subscripted builtin, covariant in its class
- Lets the body call cls()
- Older alias lives in typing
basics
~10 stype[Digest] accepts the class object itself, Digest or any subclass, so the function can call it to build instances. A bare Digest annotation accepts an already-built instance instead.
solid answer
~40 sA bare `Digest` annotation says the argument **is** a digest; `type[Digest]` says the argument is the **class**, so the callee may call `cls()`, read `cls.__name__`, or park it in a registry. The relationship is covariant: `type[HtmlDigest]` satisfies `type[Digest]`, which is exactly what makes plugin tables and factories checkable. A checker resolves `cls()` against the constructor it can see on `Digest`, so a call whose arguments do not match is flagged at the call site instead of at runtime. `typing.Type[Digest]` is the older spelling of the same idea; since Python 3.9 the builtin `type[...]` is subscriptable in annotations and is the preferred form. Bare `type` with no parameter means *any class object at all* and throws away every one of those guarantees.
code
python · 12 linesclass Digest:
def render(self) -> str:
return "digest"
class HtmlDigest(Digest):
pass
def build(cls: type[Digest]) -> Digest:
return cls()
made = build(HtmlDigest)
print(type(made).__name__, made.render())go deeper
Be ready to state the one-line difference: Digest is an instance, type[Digest] is the class object you can call. Recognise it in a factory signature and know a subclass may be passed.
Explain the mechanics: covariance in the class parameter, the constructor signature being checked through cls(), class attributes such as __name__ resolving, and why bare type gives all of that up.
Show where it earns its keep in production code — registries, plugin loaders, wiring helpers — and name the two limits: an abstract class is refused, and the concrete subclass is erased from the return type unless you thread a type variable through.
Own the API-shape call: whether a subsystem's extension point should take a class object at all, or a callable factory, or a protocol. Each choice fixes what plugin authors may register and how hard the boundary is to evolve later.
## A class is a value, and it needs its own annotation Everything in Python is an object, classes included. `Digest` is a name bound to a class object whose own type is `type`. That means a class can be passed as an argument, stored in a dict, returned from a function — and when you do that, the annotation has to describe *the class*, not an instance of it. That is the whole job of `type[C]`: - `def send(d: Digest) -> None` — the caller hands over a **built digest**. The body may call `d.render()`. - `def send(cls: type[Digest]) -> None` — the caller hands over the **class**. The body may call `cls()` to make one, read `cls.__name__`, or use it in `issubclass`. Getting this backwards is one of the most common early annotation mistakes, and it is silent: at runtime both work fine until the first attribute access, because Python does not enforce annotations. ## What the checker actually knows `type[Digest]` buys three concrete guarantees. **Subclasses are allowed.** `type[C]` is covariant in `C`: if `HtmlDigest` derives from `Digest`, then `type[HtmlDigest]` is a valid `type[Digest]`. This is what makes a registry work. A dict annotated `dict[str, type[Digest]]` can hold every concrete digest class in the codebase, and pulling one out and calling it yields something the checker knows is at least a `Digest`. **The constructor is checked.** When the body calls `cls()`, the checker looks at the visible `__init__` (or `__new__`) signature on `Digest` and validates the arguments. Write `cls(subject, "extra")` against a one-argument constructor and you get an error before the code ships. A bare `type` annotation cannot do this: `type` alone means *some class*, its constructor signature is unknown, and calls through it are unchecked. **Class attributes resolve.** `cls.__name__` is a `str`, and a `ClassVar` declared on `Digest` is visible through `cls` too. Under a bare `Digest` annotation, `d.__name__` is an error, because instances do not carry it. ## The old spelling and the new Before Python 3.9 the builtin generics were not subscriptable at runtime, so the typing module shipped aliases: `typing.Type[Digest]`, `typing.List[int]`, and friends. PEP 585 made the builtins themselves subscriptable in 3.9, and `type[Digest]` has been the recommended spelling since. `typing.Type` still exists and still works on 3.14 — it is deprecated, not removed — so you will meet it in older code and should read it as a synonym. New code should not use it. Two adjacent forms are worth keeping straight: - **`type[Digest]`** — the class `Digest` or a subclass of it. - **`type`** — any class object whatsoever, with no constructor knowledge. Use it only for genuinely open code such as a `__class_getitem__` helper or a debugging utility. ## Where it shows up in real code Factories and registries, almost exclusively. A dispatcher that maps a format name to a renderer class, a plugin loader that collects subclasses, a dependency wiring helper that is handed a class and constructs it — every one of these takes a class object, and every one of them is a silent bug factory without the annotation. ```python REGISTRY: dict[str, type[Digest]] = {} def register(name: str, cls: type[Digest]) -> None: REGISTRY[name] = cls def build(name: str) -> Digest: return REGISTRY[name]() ``` The checker will now stop you from registering a non-`Digest` class, stop you from registering an *instance* by mistake, and tell you that `build` returns a `Digest`. ## The limitation to know about `type[Digest]` guarantees the value is a constructible `Digest` subclass. That promise is why a checker refuses an **abstract** class in that position: an abstract base cannot be called, so it cannot honour the contract. It is also why `type[Digest]` will not accept a factory *function* or a `functools.partial` — those are callables that produce digests, but they are not class objects, and `Callable[..., Digest]` is the annotation for them. The other limitation is precision. `def build(cls: type[Digest]) -> Digest` throws away which subclass came in: hand it `HtmlDigest` and the checker only knows you got back a `Digest`. Threading a bound type variable through the signature is the fix for that, and it is the natural next step once the basic form is in place.
- What does a bare `type` annotation give up compared with `type[Digest]`?Everything specific. Bare `type` means *some class object*, so a checker cannot validate the arguments you pass when you call it, cannot tell you what the call returns beyond `object`, and cannot stop you passing an unrelated class. Reserve it for genuinely class-agnostic helpers such as introspection or logging utilities; anywhere you intend to construct something, name the base.
- Is `type[Digest]` covariant or invariant in `Digest`, and why does it matter?Covariant: `type[HtmlDigest]` is accepted where `type[Digest]` is expected, because any instance the subclass produces is still a `Digest`. It matters because registries and plugin tables are built entirely out of subclasses — an invariant `type[C]` would reject every real entry and force casts. The direction is safe precisely because the value is only ever read out and called.
- Should new code still write `typing.Type[Digest]`?No. `type[Digest]` has been the recommended spelling since PEP 585 landed in Python 3.9, and it needs no import. `typing.Type` remains in the standard library on 3.14 and is documented as deprecated, so treat it as a synonym when reading existing code and normalise it when you touch the file.
Digest annotates the cookie; type[Digest] annotates the cookie cutter. You can bake with the cutter, and any cutter that stamps a Digest-shaped cookie will do.
saying these in an interview costs you the question
- Says `type[Digest]` and `Digest` mean the same thing
- Thinks `type[Digest]` accepts an instance of Digest
- Claims subclasses are rejected by `type[Digest]`
- Uses bare `type` and expects constructor argument checking
- Believes annotations are enforced at runtime by the interpreter
- Thinks `typing.Type` was removed and no longer works