Subclassing generics with type parameter as default is ambiguous at runtime

Consider the subclassing rule in the typing spec with PEP 696 type parameter defaults:

from typing_extensions import Generic, TypeVar

T = TypeVar("T")
DefaultStrT = TypeVar("DefaultStrT", default=str)


class SubclassMe(Generic[T, DefaultStrT]):
    x: DefaultStrT


class Foo(SubclassMe[float]): ...


Foo[str]  # Invalid: Foo cannot be further subscripted

Notably, Foo cannot be further subscripted, since non-overridden defaults should be substituted.

However, using another type parameter within scope as default behaves differently at runtime:

StartT = TypeVar("StartT", default=int)
StopT = TypeVar("StopT", default=StartT)
StepT = TypeVar("StepT", default=int | None)


class slice(Generic[StartT, StopT, StepT]): ...


class int_slice(slice[int]): ...


int_slice[int]  # Should be invalid, but OK at runtime

Here, slice[int] substitutes StartT with int and StopT with StartT, but int_slice still keep StartT in__parameters__, making it further subscriptable at runtime. There are cases where a subclass explicitly specifies it can be further parameterized, say,

class from_int_slice(slice[int, StopT]): ...

but AFAIK there is no way to distinguish from_int_slice from int_slice at runtime as they both contain effectively subscriptable type parameter.

While I don’t expect typing implementation to also resolve default type parameters to their value within scope, the subclassing rule seems to imply that non-overridden defaults should be excluded from __parameters__ in subclasses as well. Hoping to clarify if this is intended behavior!

1 Like