How does TypeVar('T', bound=X) differ from TypeVar('T', X, Y)?
answer
- One is open, one is closed
- Upper limit versus a menu
- Does the subclass survive the call?
- Mixing two listed types is rejected
- bound preserves, constraints widen
basics
~20 sbound=X is an upper bound: the parameter may be solved to X or any subtype, and the solution keeps the subtype. A value-constraint list means the parameter must be solved to exactly one of the listed types, and a subclass is widened to the listed one.
solid answer
~40 s`TypeVar("S", bound=Stop)` says *anything that is a Stop*: the checker will solve `S` to `Depot` if you pass a `Depot`, so the return type stays `Depot`, and any subclass written later fits automatically. `TypeVar("N", str, bytes)` is a **value-constraint list**: the parameter must be solved to one of those exact types, the set is closed, mixing them in one call is rejected, and a `str` subclass is widened up to `str` rather than preserved. So use a bound when the point is "has this interface, keep the caller's concrete type", and constraints when the function genuinely has one implementation per listed type and mixing is meaningless — the `str`/`bytes` split being the canonical case. A bound may also name a protocol or a string forward reference; constraints require at least two types.
code
python · 22 linesfrom typing import TypeVar
class Stop:
def label(self) -> str:
return "stop"
class Depot(Stop):
def label(self) -> str:
return "depot"
S = TypeVar("S", bound=Stop) # any subtype of Stop, now or later
N = TypeVar("N", str, bytes) # exactly str or exactly bytes, never mixed
def announce(place: S) -> S:
print(place.label())
return place
def join(a: N, b: N) -> N:
return a + b
announce(Depot())
print(join("a", "b"), join(b"a", b"b"))go deeper
Recall the headline: bound means 'this type or a subtype', a list of types means 'exactly one of these'. Knowing that the str/bytes split is the classic constraint-list case is enough at this level.
Explain the two consequences of constraints an interviewer is fishing for: mixing two listed types in one call is rejected, and a subclass is widened to the listed type instead of preserved. Then say why a bound is the default choice.
Show you can spot the misuse in review — a constraint list enumerating a class and its subclasses — and explain the maintenance cost when a new subclass appears later. Know when a Protocol bound beats a nominal base.
Own the convention: which shared helpers may narrow their parameters at all, whether the codebase prefers structural Protocol bounds over inheritance, and how a widened return type quietly erodes type information across module boundaries.
## Two different questions Both forms narrow what a type parameter may be solved to, but they answer different questions. * **`bound=`** answers *what must this type be able to do?* It sets an **upper bound**: the solution may be the bound itself or any subtype of it. * **A value-constraint list** answers *which of these exact types is it?* It enumerates a **closed set**, and the solution must be one member of that set. ```python from typing import TypeVar S = TypeVar("S", bound=Stop) # Stop or any subtype N = TypeVar("N", str, bytes) # exactly str, or exactly bytes ``` ## What the checker does differently With a bound, solving is ordinary subtype inference and the **concrete argument type survives**. Pass a `Depot` (a subclass of `Stop`) to `def announce(place: S) -> S` and the call's type is `Depot`, so the caller can still use `Depot`-only members on the result. Inside the body you may use anything `Stop` provides, because every possible solution is at least a `Stop`. With constraints, the checker type-checks the *body once per listed type* and, at a call site, picks the single member the arguments fit. Two consequences follow, and both are commonly missed: 1. **Mixing is rejected.** For `def join(a: N, b: N) -> N` with `N` constrained to `str` and `bytes`, `join("a", b"b")` has no single solution and is an error — which is precisely the point, since concatenating the two is a runtime `TypeError` anyway. 2. **Subclasses are widened.** Pass an instance of a `str` subclass and the parameter is solved to `str`, not to the subclass; the return type comes back as `str`. A bound would have preserved it. ## When each is right Reach for a **bound** in the overwhelming majority of cases: * the function calls methods on the value, so it needs a guaranteed interface; * the set of acceptable types is open — callers add subclasses you have never seen; * the caller should get its own concrete type back rather than a widened one. The bound need not be a class: it may be a `Protocol`, so the constraint is structural ("anything with these methods") rather than nominal, and it may be written as a string forward reference when the class is defined later in the file. Reach for **constraints** only when the function truly has a separate meaning per listed type and no meaningful common supertype: the `str`/`bytes` split is the archetype, and `typing.AnyStr` was the stdlib's own instance of exactly that pattern. The list must contain **at least two** types — a one-element constraint list is an error, because it would just be a rename of that type — and you may not combine `bound=` with constraints in one declaration. ## A concrete way it goes wrong Consider a route-optimisation job whose helper is meant to accept any kind of stop and hand back the same kind. Written with constraints as `TypeVar("S", Stop, Depot)`, the code passes review and then misbehaves in two ways at once: a `Warehouse` subclass added three weeks later is rejected outright even though it is a perfectly good `Stop`, and passing a `Depot` may be solved to `Stop`, so the call site loses the `Depot`-only attributes it wanted. With `bound=Stop` both problems vanish — new subclasses are accepted for free and the concrete type is preserved. The rule of thumb: if you find yourself listing a base class *and* its subclasses in a constraint list, you wanted a bound. ## Runtime and declaration details Both forms are recorded on the `TypeVar` object at declaration time and neither is enforced when the program runs; a call that a checker rejects still executes. The bound expression is evaluated lazily on modern versions, which is why a string forward reference works. And note the asymmetry in reading the call: `TypeVar("N", str, bytes)` has no keyword to signal its meaning — positional arguments after the name *are* the constraint list, which is why the two forms are so easily confused in review. Say it out loud when you read one: "bound means subtypes allowed and preserved; a list means exactly these, widened."
- Can one TypeVar declaration have both a bound and a constraint list?No — the two are mutually exclusive and combining them is an error at declaration time. A constraint list must also hold at least two types, since a single-element list would merely rename that type. If you need 'one of these, but keep subtypes', the honest spelling is a bound on a common base or a Protocol, not a hybrid.
- Why does a str subclass come back as plain str from a constrained parameter?Constraints are solved to one listed member, so the checker maps the argument up to the nearest listed type and substitutes that into the return. The subtype information is deliberately discarded, because the body was checked only against the listed types. A bound solves to the argument's own type instead, so the subclass survives to the call site.
- What does using a Protocol as the bound buy you over a base class?Structural rather than nominal matching: any type with the required members satisfies it, with no inheritance and no registration, including types from libraries you do not control. That keeps the parameter open in the way a bound is meant to be, and avoids forcing an inheritance relationship purely to satisfy the type system.
A bound is a job requirement — 'must hold a driving licence' — and whoever turns up keeps their own identity. A constraint list is a dropdown with two options: you pick one entry exactly, and anything close to an entry is recorded as that entry.
saying these in an interview costs you the question
- Says a constraint list also accepts subclasses
- Thinks bound= narrows to that exact type only
- Lists a base class and its subclasses as constraints
- Expects the return to keep a subtype under constraints
- Combines bound= with positional constraint types
- Believes either form is checked at runtime