When subclassing a typing.Generic base, how do Base[int] and Base[T] differ?
answer
- Fixed, passed through, or forgotten
- Does the subclass still take a parameter?
- Bare base means Any everywhere
- Generic[T] is redundant after Box[T]
- isinstance against a subscript raises TypeError
basics
~20 sclass C(Base[int]) fixes the parameter: C is an ordinary non-generic class whose inherited members are all typed with int. class C(Base[T]) passes the parameter through, so C is itself generic and callers write C[str]. Inheriting bare Base fixes nothing and silently means Base[Any].
solid answer
~40 sSubscripting the base with a concrete type — `class IntBox(Box[int])` — **specialises** it: the type parameter is bound at class-definition time, every inherited annotation reads `int`, and `IntBox` takes no parameter of its own, so `IntBox[str]` is an error. Subscripting with a TypeVar the subclass also carries — `class LoggedBox(Box[T])` — **propagates** it: the subclass is generic in `T`, and you do not need to list `Generic[T]` again because the parameterised base already declares it. Inheriting the bare class, `class SloppyBox(Box)`, is the trap: it is treated as `Box[Any]`, so every inherited member de-types and no error is reported. At runtime none of these change behaviour — subscripting the base is erased, and instances are ordinary objects of the subclass.
code
python · 20 linesfrom typing import Generic, TypeVar, get_args, get_origin
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, item: T) -> None:
self.item = item
class IntBox(Box[int]): # parameter fixed: IntBox is not generic
pass
class LoggedBox(Box[T]): # parameter passed through: still generic
pass
print(get_origin(LoggedBox[str]), get_args(LoggedBox[str]))
print(IntBox(7).item)
try:
isinstance(Box(1), Box[int])
except TypeError as exc:
print("TypeError:", exc)go deeper
Recall the two spellings and what each means: a concrete argument in the base fixes the type for good, a TypeVar in the base keeps the subclass generic. Know that annotations do nothing at runtime here.
Explain why the extra Generic[T] base is redundant, what an unsubscripted base silently means, and how an override's signature changes once the parameter is fixed. Be able to write all three forms without hesitating.
Show you would catch the bare-base case in review and know the checker setting that surfaces it, and reason about which subclasses in a hierarchy should specialise versus stay parameterised so behaviour extensions do not multiply per element type.
Own the hierarchy's shape: how deep generic inheritance should go before it costs more in signature noise than it returns, and whether a public base is exported parameterised or as concrete specialisations for other teams to consume.
## Three ways to inherit, three meanings Given `class Box(Generic[T])`, a subclass may write its base three ways, and they mean different things. **1. Fixed (specialised):** ```python class IntBox(Box[int]): ... ``` The parameter is bound once, at class-definition time. `IntBox` is a plain, non-generic class: every inherited annotation that said `T` now says `int`, `IntBox()` needs no type argument, and writing `IntBox[str]` is an error because there is no parameter left to fill. This is the right shape when the subclass exists precisely to nail the type down — a repository of one specific row type, a queue of one message type. **2. Passed through (still generic):** ```python class LoggedBox(Box[T]): ... ``` The subclass hands its own parameter to the base, so `LoggedBox` is generic in `T` and callers write `LoggedBox[str]`. A detail that surprises people: **you do not need to list `Generic[T]` as an extra base**. Subscripting an already-generic base with a TypeVar declares the subclass's parameter for you. Writing `class LoggedBox(Box[T], Generic[T])` is legal and merely redundant; where the explicit `Generic[...]` base still earns its place is when you want to *reorder* parameters or declare one the bases do not supply. **3. Bare (the trap):** ```python class SloppyBox(Box): ... ``` An unsubscripted generic base is implicitly `Box[Any]`. Nothing is reported, and every inherited member quietly loses its type: `SloppyBox().item` is `Any`, and anything built from it is unchecked. Because it looks like ordinary inheritance, this is the version that survives review — a checker configured to flag missing type parameters is what catches it. ## Partial specialisation with several parameters With a base taking two parameters, a subclass may fix one and keep the other: `class StrKeyed(Mapping2[str, V])` leaves `V` free while pinning the key type. The subclass's parameter list is exactly the set of TypeVars appearing in its bases, in the order they first appear — which is why `Generic[...]` is sometimes still written explicitly, to state that order rather than inherit it. ## Overriding methods after specialising Once a base is fixed to `Box[int]`, overriding an inherited method must use the *substituted* signature, not the parameterised one: an override of a method the base declared as `def put(self, item: T) -> None` takes `int`. Widening it back to `T` is not an override of anything the subclass has — the parameter no longer exists there. ## What runtime does and does not keep Subscripting is erased. `IntBox(7)` is an ordinary instance whose `type()` is `IntBox`, holding no record that the base was `Box[int]`. Subscripting a generic class produces an alias object rather than a new class, and `typing.get_origin` / `typing.get_args` will read it back apart: ```python get_origin(LoggedBox[str]) # LoggedBox get_args(LoggedBox[str]) # (str,) ``` And `isinstance(obj, Box[int])` raises `TypeError` — *subscripted generics cannot be used with class and instance checks* — because the parameter was never stored on the instance to check against. Use `isinstance(obj, Box)` when you need a runtime check; it answers the class question only, which is all the runtime knows. ## Choosing between the three Fix the parameter when the subclass names a specific use and nothing downstream should vary. Pass it through when the subclass adds behaviour — logging, caching, validation — that is orthogonal to the contained type, since fixing it there would force one subclass per element type. Never leave the base bare: if you genuinely mean "any contents", write `Box[Any]` explicitly, so the next reader sees a decision rather than an omission, and so a stricter checker configuration does not turn a silent hole into a surprise the first time someone tightens the settings.
- Is there ever a reason to list Generic[T] as a base alongside an already-parameterised base?Yes — to control the parameter order, or to declare a parameter the bases do not supply. A subclass's parameter list is otherwise the TypeVars found in its bases in first-appearance order, so when a class inherits two parameterised bases and callers should subscript it in a different order, the explicit Generic base states that order. Otherwise it is redundant.
- What does inheriting the bare generic class do to a checker's view of inherited members?It treats the base as parameterised with Any, so every inherited annotation mentioning the parameter becomes Any and stops being checked. Nothing is reported by default, which is what makes it the most common silent hole in a generic hierarchy; a checker setting that flags missing type parameters turns it into a visible error.
- How do you check at runtime whether an object came from a generic class?Test against the unsubscripted class: `isinstance(obj, Box)` works, while `isinstance(obj, Box[int])` raises TypeError because the argument was never stored on the instance. If you need the argument at runtime you must record it yourself — for example by storing it in an attribute at construction — since the type system keeps nothing.
Inheriting Box[int] is ordering a shelf built for one size of box; inheriting Box[T] is inheriting the adjustable bracket, still to be set by whoever mounts it. Inheriting bare Box is mounting the bracket and never tightening it.
saying these in an interview costs you the question
- Thinks a subclass of Box[int] can still be subscripted
- Adds Generic[T] believing Box[T] alone is insufficient
- Sees no problem with inheriting the bare generic class
- Expects isinstance to work against a subscripted generic
- Believes Box[int] creates a distinct runtime class
- Overrides an inherited method with the unsubstituted signature