Iâm looking at the PEP again to see whether the use cases are strong enough to motivate a change to the core language. I feel that for this PEP to be accepted, it should give good examples of sound, type-safe code that is currently impossible or unduly verbose to write. Letâs go over the examples given in the PEPâs âMotivationâ section.
Empty containers
def make_list[T](*args: T) -> list[T]: ...
reveal_type(make_list[int]()) # type is list[int]
This makes sense for the specific case of empty containers. Why would you write such a function, though? Are there examples of real users who needed this?
Factory callable
def factory[T](func: Callable[[T], Any]) -> Foo[T]: ...
reveal_type(factory[int](lambda x: "Hello World" * x)) # type is Foo[int]
This is a good example:
- I can imagine a type-safe implementation
- Type checkers would be able to tell if you use the wrong type. For example, if you wrote
factory[str]
on the second line, type checkers can flag that "Hello World" * str
would fail.
- Type checkers canât realistically infer this type, because other types could potentially exist that can be multiplied with a string.
On the other hand, there is a trivial workaround that works today: you can write my_foo: Foo[int] = factory(lambda x: "Hello World" * x)
. Guido rightly points out above that this may be less convenient if the type is more complicated than just Foo[int]
. Still, how common is this sort of thing in practice? Can you point to e.g. mypy issues where someone writing real code would have been helped by this?
Undecidable inference
def foo[T](x: Sequence[T] | T) -> list[T]: ...
reveal_type(foo[bytes](b"hello")) # list[bytes]
Here the function subscript helps the type checker decide whether to pick the Sequence[T]
or the T
branch of the union. But that is problematic, because it seems I could as well write foo[int](b"hello")
and get list[int]
back (since bytes
is a Sequence[int]
).
Yet at runtime, foo
will only return either list[int]
or list[bytes]
, because its implementation canât see whether we did foo[int]
or foo[bytes]
. Therefore, I donât think there is a type-safe way to implement this foo
function.
If we want to fix this undecidable inference, I feel we should do it instead by providing a more precise specification for type checkers on how to solve this type variable.
Unsolvable type parameters
def foo[T](x: list[T]) -> T: ...
reveal_type(foo[int]([])) # type is int
This is another function that is impossible to implement: how can it return an instance of an arbitrary type when given an empty list?
Conclusion
This PEP needs a stronger motivation from real use cases. Two of the examples are useful, but I am not convinced that they represent common real-world code. Two others involve functions that cannot be implemented in a type-safe way.
I vaguely remember several cases over years of following typing discussions where subscripting functions has come up. However, the PEP does not link to any such cases, so I have to evaluate it based on the examples given in the PEP itself. Right now, those examples arenât convincing enough for me to support the PEP.
(Speaking only for myself, not the Typing Council.)