A factory annotated `(cls: type[Digest]) -> Digest` forces callers to cast the result — how do you fix it?
answer
- The return forgot the argument
- Casts at every call site
- Link two positions with one variable
- Bound keeps the constructor checkable
- Inline syntax arrived in 3.12
basics
~10 sThread 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.
solid answer
~50 s`(cls: type[Digest]) -> Digest` erases which subclass arrived: hand it `HtmlDigest` and the checker only knows the result is a `Digest`, so any subclass-specific attribute needs a cast — and every cast is a place a later refactor can lie. Replacing the fixed class with a bound type variable links the two positions: `def build[T: Digest](cls: type[T]) -> T` says *whatever concrete class you give me comes back*. The bound keeps the constraint that made the original signature useful, so `cls()` is still checked against `Digest` and unrelated classes are still refused. In Python 3.12 and later the inline `[T: Digest]` syntax from PEP 695 is the modern spelling; the equivalent older form declares a module-level `TypeVar` with a `bound`. The pattern is only honest when the function genuinely returns an instance of the class it was handed.
code
python · 11 linesclass Digest:
pass
class HtmlDigest(Digest):
def inline_css(self) -> str:
return "body{margin:0}"
def build[T: Digest](cls: type[T]) -> T:
return cls()
print(build(HtmlDigest).inline_css())go deeper
Recall the symptom rather than the cure: if a factory is annotated to return the base class, the checker forgets which subclass you passed and complains about subclass-only attributes.
Explain the mechanics of the fix — one type variable appearing in both the parameter and the return, solved per call site, with a bound that preserves the constructor check — and write the 3.12 inline syntax correctly.
Demonstrate the judgement: casts accumulate and go stale, so fix the signature instead; and know when type[T] -> T becomes a lie, such as a pooled or fallback factory that may hand back a different subclass.
Own how precise a shared factory's contract should be. Over-precise signatures constrain future implementations — caching, proxying, fallbacks — while loose ones push casts into every consumer; decide which cost the codebase should carry.
## The problem: a fixed class in the return position erases information Take a digest-sending service whose factory constructs whichever renderer the caller names: ```python def build(cls: type[Digest]) -> Digest: return cls() ``` At runtime this is perfect — `build(HtmlDigest)` really does return an `HtmlDigest`. Statically it is lossy. The return annotation says `Digest`, full stop, so the checker has forgotten the argument by the time it types the result: ```python d = build(HtmlDigest) d.inline_css() # error: "Digest" has no attribute "inline_css" ``` The usual reaction is a cast, and casts are corrosive. A `cast` is an unchecked assertion; it stays in the file after somebody changes `build` to return a cached instance of a different subclass, and then it is simply a lie that the checker has been instructed to believe. One cast is a nuisance; a codebase where every call to the factory is followed by one has turned the type system off at exactly the place it was most useful. ## The fix: link the argument and the return with a bound type variable ```python def build[T: Digest](cls: type[T]) -> T: return cls() ``` Read it as a rule rather than a type: *for any `T` that is a `Digest` or a subclass of one, given the class `T`, you get back a `T`.* The checker now solves `T` at each call site from the argument. `build(HtmlDigest)` resolves `T` to `HtmlDigest`, and `d.inline_css()` type-checks without a cast. `build(PlainDigest)` resolves it to `PlainDigest` in the same expression, which is what makes this different from just annotating the concrete class — one function serves every subclass at full precision. The **bound** is doing real work and is not decoration. Without it, `T` is unconstrained: `cls()` could not be validated against any known constructor, and `build(int)` would be accepted. With `T: Digest`, the checker still knows the constructor signature it is calling, still refuses unrelated classes, and still refuses an abstract class in that position — you keep every guarantee of the original signature and gain precision. ## The two spellings PEP 695 added the inline type-parameter syntax in Python 3.12, and `def build[T: Digest](...)` is the current form: the variable is scoped to the function, needs no import, and reads in one line. The older spelling declares the variable separately with a keyword bound and is what you will meet in any codebase that still supports 3.11, and it means exactly the same thing to a checker. Python 3.13 added defaults for type parameters (PEP 696), which matters for generic classes more than for a factory like this one. ## Where the pattern is honest and where it lies `type[T] -> T` asserts that the returned object *is an instance of the class passed in*. That is true for `return cls()` and for anything that goes on to return that same object. It stops being true the moment the body can substitute something else: - a cache or pool that may return an instance of a **different** registered subclass - a factory that falls back to a default renderer when the requested one is unavailable - a body that wraps the instance in a decorator or proxy object In those cases the precise signature is a false promise, and the checker will happily let callers reach for attributes that are not there at runtime. The honest annotation is then the base class again, or a union, or a redesign that separates the exact-construction path from the fallback path. A second failure mode is the *unused* type variable. If `T` appears only once in a signature, it is not linking anything and is almost always a mistake — `def build[T: Digest](cls: type[T]) -> Digest` gains nothing over the plain form. ## The neighbouring tool When the method lives **on** the class and should return the caller's own class, the dedicated annotation for that is the special self type rather than a hand-rolled type variable; it is shorter and it composes correctly through inheritance. The `type[T]` pattern here is for the other shape: a free function or a service method that is *handed* a class from outside — a registry lookup, a plugin loader, a wiring helper — and constructs it. That is the case where the class object is data, and the bound type variable is what keeps it from decaying into the base type on the way back out.
- What breaks if you drop the bound and write `def build[T](cls: type[T]) -> T`?You keep the precision and lose the constraint. `T` becomes unconstrained, so the checker no longer knows the constructor it is calling through `cls()`, cannot validate the arguments, and will accept an entirely unrelated class. The bound is what carries over the guarantee the original `type[Digest]` signature had; the type variable only carries over the identity.
- When is `type[T] -> T` the wrong annotation for a factory?Whenever the body may return something other than an instance of the class it was handed — a pooled instance of a different subclass, a fallback renderer when the requested one is missing, or a wrapper object. The signature then makes a promise the runtime does not keep, and callers will access attributes that are not there. Annotate the base, a union, or split the paths.
- How does this differ from a method that returns the caller's own class?Different shape and a different tool. Here the class arrives as an argument from outside, so a bound type variable links the parameter to the return. A method that should return whatever concrete class it was invoked on has a dedicated self type annotation that composes through inheritance without any type variable at all.
The fixed signature is a coat check that hands back a coat; the bound type variable is one that hands back your coat, while still refusing anything that is not a coat.
saying these in an interview costs you the question
- Reaches for a cast at every call site instead
- Drops the bound and loses constructor checking
- Uses a type variable that appears only once
- Keeps `-> T` on a factory with a fallback path
- Thinks the annotation changes what runs at runtime
- Claims PEP 695 syntax works on Python 3.11