skip to content

questions

4

In Python typing, how does a parameter annotated `type[Digest]` differ from one annotated `Digest`?

level: juniorimportance: must knowfreq 45%

answer

  1. Classes are values too
  2. Cookie cutter, not the cookie
  3. Subscripted builtin, covariant in its class
  4. Lets the body call cls()
  5. Older alias lives in typing

basics

~10 s

type[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 s

A 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 lines
python
class 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

Why does a type checker reject an abstract class passed to a `type[Digest]` parameter?

level: middleimportance: should knowfreq 32%

basics

~20 s

type[Digest] promises the callee may call the class to build an instance. An abstract base cannot be called — doing so raises TypeError — so checkers refuse it at the call site rather than let the failure reach runtime.

open as a page

A factory annotated `(cls: type[Digest]) -> Digest` forces callers to cast the result — how do you fix it?

level: seniorimportance: should knowfreq 28%

basics

~10 s

Thread a type variable bound to Digest through the signature: def build[T: Digest](cls: type[T]) -> T. The checker then returns the exact class that was passed in, so callers keep subclass members without casting.

open as a page

When is `Callable[..., Digest]` a better factory-parameter annotation than `type[Digest]`?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

Use 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.

open as a page