I think Jukka hit the nail on the head with his analysis of the problem with angle brackets -- it would be a shame if you couldn't split a list of typevars across a line. This wouldn't seem a problem if you just have a few typevars with short names, like class C<S, T>, but it could become a problem if we add syntax for upper bounds, which would also be needed.
Currently:
T = TypeVar("T", bound=str|int)
def f(a: T) -> list[T]:
...
That would have to become something like this (borrowing from TypeScript -- we could use some other syntax instead of an 'extends' (soft) keyword):
def f<T extends int | str>(a: T) -> list[T]:
...
If you have two typevars, each with their own upper bound, splitting them across multiple lines could be considered more readable -- compare:
def f<T extends int | str, S extends list[int] | list[str]>(a: T, b: S) -> tuple[S, T]:
...
to:
def f<
T extends int | str,
S extends list[int] | list[str]
>(a: T, b: S) -> tuple[S, T]:
...
But given the number of tools that currently know about Python indents and know they should not look for indents inside (parenthes), [brackets] and {braces}, adding <angle brackets> to the list but only in `def` and `class` statements would be a big hurdle to take
Now, it makes me a bit sad that this is the case, because for readability I find angle brackets slightly easier on the eyes than square brackets, but I don't see a way around it, and it's probably not too bad. Here are the above three examples using square brackets:
def f[T extends int | str](a: T) -> list[T]:
...
def f[T extends int | str, S extends list[int] | list[str]](a: T, b: S) -> tuple[S, T]:
...
def f[
T extends int | str,
S extends list[int] | list[str]
](a: T, b: S) -> tuple[S, T]:
...
All these will look bad to old-timers, so we'd really have to fight for this, but the square brackets have the best chance. (Sorry, Sebastian, I agree with Eric that the @typevar("T") approach just won't work.)
class C[+T]:
...
(I forget if generic functions also have variance, and whether the variance is per typevar or per parameter. Jelle?)
For type aliases, it does look like the only way to solve that would be to have an 'alias' keyword. I've got nothing to add to what Jukka said. (Honestly I didn't really have much to add to what he said about angle brackets either. :-)
Finally, the conundrum of
def get_identity_func() -> Callable[[T], T]: ...
It certainly doesn't look like this corner case is worth making `Callable` a keyword (and with square brackets the syntax would be awkward). I can't quite follow Kevin Millikin's post, but if the solution is that this should be written as
def get_identity_func[T]() -> Callable[[T], T]: ...
but keep the semantics that mypy and pyright currently give it, that sounds fine to me.
Oh. A final weird thought on syntax (I can't help it, syntax is what makes Python tick for me). Maybe the square brackets with type variable definitions should go *before* the class/function name? So immediately after the keyword:
def [T] f(a: T) -> T: ...
class [T] C: ...
alias [T1, T2] ListOrDict = List[T2] | Dict[T1, T2]
This might (or might not :-) look better than having the close ']' cramped against the open '(' of the arg list. We'd be breaking new syntactic ground though -- e.g. Scala uses class Name[T] { ... } and everyone else (C#, Java, JavaScript) seems to be using class Name<T> { ... }.
I guess there's also C++ which AFAICT uses something like this:
template <class T>
class C { ... }
I don't see much of an advantage in that, except that it's the most similar to Sebastian's `@typevar("T")` proposal. If we are going to introduce a new keyword we might be able to do something like that. It would still have to use square brackets though (the problem with angle brackets is still there). A nice thing would be that it can be uniformly used to prefix `class`, `def` and type alias definitions using assignments.
Enough rambling.
--Guido