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.)


An open issue is variance. There's a proposal floating around (https://github.com/python/typing/issues/813) to use +T and -T to make T covariant or contravariant. We could easily support this with the new syntax:

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

On Tue, Mar 15, 2022 at 4:29 AM Jukka Lehtosalo <jlehtosalo@gmail.com> wrote:

I also think that Eric's post is good.

However, I don't think that angle brackets are as viable as square brackets. First, it seems less consistent than square brackets -- I agree with Kevin here. There also seems to be some potential for ambiguity: Callable<T>[[T], T], for example, is already a valid expression, where < and > are treated as binary operators. Finally, square brackets would naturally support splitting long definitions into multiple lines (in this simple example it's not necessary to use multiple lines, but in more complex examples it can be helpful):

class C[
    T,
    S,
](...):
    ...

Supporting this with angle brackets seems kind of hard (especially within expressions, where there is ambiguity with existing binary operators):

class C<
    T,
    S,
>(...):
    ...

I guess we could somehow support this, but it could be hard to justify the extra complexity in a PEP.

The alternative of using backslashes looks pretty bad, I think:

class C< \
    T, \
    S, \
>(...):
    ...

Of the Eric's proposed ways of expressing type aliases, only the idea with a new keyword seems practical to me if we use square brackets. This is the first alternative that Eric mentioned:

ListOrDict<T1, T2> = List<T2> | Dict<T1, T2>

If we'd use square brackets instead, this would be syntactically identical to an indexed assignment, which wouldn't define the type alias, T1 or T2, and thus wouldn't work at runtime:

ListOrDict[T1, T2] = List[T2] | Dict[T1, T2]

The second idea is similarly ambiguous and looks like a regular generic type annotation when using square brackets, unless TypeAlias is a keyword. Again, T1 and T2 won't be defined at runtime:

ListOrDict: TypeAlias[T1, T2] = List[T2] | Dict[T1, T2]

The third idea has the same issue if using square brackets -- it already has different meaning currently and doesn't define T1 or T2:

ListOrDict: TypeAlias = type_alias[T1, T2](List[T2] | Dict[T1, T2])

If we introduce a new keyword (only treated as a keyword in this context), there would be no ambiguity and everything would work as expected, I think -- even with square brackets:

alias ListOrDict[T1, T2] = List[T2] | Dict[T1, T2]

Jukka
_______________________________________________
Typing-sig mailing list -- typing-sig@python.org
To unsubscribe send an email to typing-sig-leave@python.org
https://mail.python.org/mailman3/lists/typing-sig.python.org/
Member address: guido@python.org


--
--Guido van Rossum (python.org/~guido)
Pronouns: he/him (why is my pronoun here?)