I think people who don’t write a lot of typing code may not realize how much of a pain it is to have to write type variables everywhere, check that they’re right, etc.
But code is also about readers, and this brings another point (sorry for the giant edit) that I think may have been missed from the discussion (although it’s mentioned in the PEP) is how the new syntax is significantly more logical the previous syntax. Type variables have an intuitive scope no matter how you declare them. Even if you declare a type variable the old way:
T = TypeVar('T')
it doesn’t have any real meaning outside some generic object. And it can take significant effort for the reader to figure out what that object is. Consider:
class X(Generic[T]):
def f(self, a: T) -> None: ...
def g(self, b: U) -> None: ...
Here, T
is scoped to the class X
, and is meaningful anywhere inside it, but U
is scoped to the function g
, and is only meaningful inside it. While T
’s scope is made obvious by inheriting from Generic[T]
, U
’s scope is not obvious, and the reader has to carefully check every enclosing function and class.
Ideally, the declaration should be right beside the start of the scope in which it’s valid. And while there is a reasonable place to do that for classes (in the inheritance list), there is currently no such place for functions. And ideally, that point for functions would be before the signature because the signature depends on the type variables.
Also, ideally, whatever notation we choose for functions should be the same for classes just to reduce cognitive load.
This PEP satisfies all of these things:
- it defines type variables at the scope in which they are valid,
- it defines function-scoped type variables before the signature, and
- it uses the same notation for generic classes, functions, and type variables.
I understand the desire for conservatism, and I think we should definitely explore other possibilities, but so far, I think this notation seems to be the most logical to me from a typing perspective.