skip to content

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

level: middleimportance: should knowfreq 32%

answer

  1. The annotation promises more than ancestry
  2. Whoever receives it may call it
  3. abc metaclass blocks instantiation
  4. Concrete subclasses are the intended argument
  5. Only concrete class can be given

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.

solid answer

~50 s

The annotation `type[Digest]` is a construction promise, not merely a subclass claim: whoever receives that argument is entitled to call `cls()`. A class with unimplemented `abc.abstractmethod` members cannot be instantiated — the `abc.ABCMeta` machinery raises `TypeError` at the call — so passing the abstract base itself would break the promise, and checkers report it as *only concrete class can be given*. Concrete subclasses that implement every abstract method are accepted normally, which is the intended use. If you want the abstract class as a **value** rather than a constructor — as a registry key, or for an `issubclass` test — that is a different contract, and the honest fixes are to annotate the parameter for what you actually do with it or to narrow the API so only concrete leaves are registered. Silencing it with a cast keeps the runtime `TypeError`.

code

python · 13 lines
python
from abc import ABC, abstractmethod

class Digest(ABC):
    @abstractmethod
    def render(self) -> str: ...

def build(cls: type[Digest]) -> Digest:
    return cls()

try:
    build(Digest)
except TypeError as exc:
    print(exc)

go deeper

for a junior

Remember the shape of the failure: calling an abstract class raises TypeError, so a checker will not let you hand that class to something that constructs it. Pass a concrete subclass instead.

for a middle

Explain the mechanics — abc.abstractmethod plus the abc.ABCMeta guard at instantiation, and the checker rule that mirrors it — and know that an abstract class remains a perfectly good annotation for an instance parameter.

for a senior

Show why the static rule matters operationally: an abstract entry in a registry plus a broad except around construction hides the failure entirely. Argue for deleting the entry or reshaping the API rather than casting the error away.

for a principal

Own the extension-point contract: decide whether plugin authors register classes, factories, or protocol-conforming objects, and how the base advertises which of its subclasses are meant to be constructed at all.

## The annotation is a promise about calling, not just about ancestry It is tempting to read `type[Digest]` as *a class in the `Digest` hierarchy*. It is stronger than that. Because the receiving code is allowed to write `cls()`, the annotation carries an implicit guarantee: **this class can be constructed**. Abstract classes cannot, so they are excluded. The runtime side is `abc`. When a class inherits from `abc.ABC` (or uses `abc.ABCMeta` directly) and leaves any `abc.abstractmethod` unimplemented, the metaclass refuses instantiation: ```python from abc import ABC, abstractmethod class Digest(ABC): @abstractmethod def render(self) -> str: ... Digest() # TypeError: Can't instantiate abstract class Digest # without an implementation for abstract method 'render' ``` A checker that let `Digest` itself flow into a `type[Digest]` parameter would be signing off on code that raises at runtime. So it reports the error where it can be fixed — at the call site that passed the class — with a message of the form *only concrete class can be given where "type[Digest]" is expected*. ## Why this rule earns its keep Consider a digest-sending service with a registry mapping a format name to the class that renders it. Someone adds the abstract base to the table by mistake — an easy slip, since the base is the name that is already imported everywhere: ```python REGISTRY: dict[str, type[Digest]] = {"html": HtmlDigest, "plain": Digest} ``` Now imagine the send loop wraps construction defensively, as tired production code does: ```python for name in due_formats: try: REGISTRY[name]().send() except Exception: pass ``` The `TypeError` from the abstract class is swallowed by that bare `except`. Plain-text subscribers simply stop receiving anything. Nothing is logged, no alert fires, and at a peak of 1,200 requests per minute the missing sends are invisible in the aggregate throughput graph — the service looks healthy because it *is* healthy for every other format. The bug survives until a human complains. A static check that refuses the abstract entry catches this before the branch merges, which is the entire argument for the rule. ## What is and is not refused - **Refused:** the abstract class object in a `type[Digest]` position. - **Fine:** `Digest` as an ordinary annotation — `def send(d: Digest)` is correct and common, because you are receiving an already-built instance and the concrete subclass is somebody else's problem. - **Fine:** any concrete subclass in the `type[Digest]` position. - **Fine:** a `typing.Protocol` used as a *bound* on an instance parameter; protocols describe shape, and only the runtime-checkable machinery cares about ancestry. A related trap: a class can be abstract in a checker's eyes without inheriting from `abc.ABC` at all. A body of `...` or `raise NotImplementedError` is **not** abstract to `abc`, so the runtime happily instantiates it and you get a broken object instead of a clean failure. Conversely, an `abc.ABC` subclass that implements every abstract method is fully concrete and passes. ## The honest fixes **Register only concrete leaves.** Most of the time the abstract entry was simply wrong, and deleting it is the fix. Keep the base out of the table and let the annotation guard it. **Say what you actually mean.** If the parameter really is a hierarchy marker — used for `issubclass` tests, for a name, or as a dict key — then it is not a constructor and should not be annotated as one. Narrow the API so the constructing path and the classifying path are separate parameters or separate functions. **Use a callable when construction is indirect.** If the caller wants to supply a partially-applied factory rather than a class, `Callable[..., Digest]` is the right annotation and abstractness stops being the question, because the callable is what gets invoked. **Do not silence it.** A cast makes the checker quiet and changes nothing at runtime: calling the abstract class still raises. Silencing plus a broad `except` is exactly the combination that produced the invisible outage above. The rule is one of the clearer cases where a type checker is enforcing a real runtime invariant rather than a stylistic preference — the annotation says *constructible*, and abstract classes are not.

  • Is `Digest` still a legal annotation elsewhere if it is abstract?
    Yes, and it is the normal one. `def send(d: Digest) -> None` is fine: you are receiving an instance, and every instance that exists is necessarily a concrete subclass. Only the class-object position is restricted, because only that position implies the receiver may construct it. Abstract bases make excellent parameter and return annotations for instances.
  • How would you type a parameter that only ever does `issubclass` on the class it is given?
    That parameter is not a constructor, so do not annotate it as one. Keep it as a class-object annotation if you also need the base relationship, but be explicit in the API that nothing is constructed — or split it, taking the class for the check and a separate factory for building. The rule bites only because `type[C]` implies construction; a design that never constructs should not carry that implication in the same parameter.
  • Does a method body of `raise NotImplementedError` make a class abstract?
    Not to the runtime. `abc` tracks abstractness only through `abc.abstractmethod` on a class whose metaclass is `abc.ABCMeta`, so a class using `NotImplementedError` alone instantiates happily and fails later, deeper, and with worse context. Use `abc.ABC` plus `abc.abstractmethod` when you want the failure at construction time and the checker's cooperation.

saying these in an interview costs you the question

  • Thinks `type[C]` only means 'a class in the hierarchy'
  • Says the abstract class is rejected as a plain annotation too
  • Believes `raise NotImplementedError` makes a class abstract
  • Silences the error with a cast and calls it fixed
  • Claims the interpreter enforces the abstract rule via annotations
  • Cannot explain why concrete subclasses are accepted

context