Implicit tuple return type


def go() -> tuple[int, str]: 
  return 1, "A"


def go() -> int, str:
  return 2, "B"

I don’t know what else to say and I think it just looks good :slight_smile:


Well, that’s a pretty weak argument for changing the syntax of the language.

For what it’s worth, I disagree: the elements of the bare tuple are not so clearly grouped to my eye, and I’d prefer def go() -> (int, str):, with parentheses to make the return type clearer.

But actually the existing syntax is fine to me.


Wouldn’t you then expect you can use it in other cases as well?

def foo(bar: (int, str)): ...
1 Like

To stretch this further:

def foo(bar: [int], baz: {str: int}) -> (int, str):

Yeah I don’t want that at all


You forgot sets: :slight_smile:

def foo(bar: [int], baz: {str: int}, qux: {int}) -> (int, str):
1 Like

What about:

def foo() -> ([int], {str}):

And would this raise an error?

def foo() -> {[int]: {str}}:

Edit: not really a question :slight_smile:

That would raise an error, lists are not hashable.

Wouldn’t the dict syntax be more expressive?

dic: {str: str, int: int} = {
    "abc": "xyz",
    123: 789

On a bit more serious note.

I think this is a fairly good proposal. The verbosity of typing has been getting on my nerves from the very beginning. Although it is much better after typing import is not needed anymore for base containers. So I am fairly fine now.

However, it would be too early to seriously consider this. This doesn’t really add anything - it is pure convenience and there is a significant risk that such implementation could screw up the path for something else which is better.

Having that said, I would love to see this reconsidered in the future. It could turn out to be a very pleasant convenience that occupies syntactic space that is just not needed for anything else.


Unfortunately, that’s not possible since x[y, z] is indistinguishable from x[(y, z)]. Under your proposal dict[int, str] would be indistinguiable from dict[tuple[int, str]].


What is a dict[tuple[int, str]]?

Actually, it does work:

class Test:
    def __class_getitem__(cls, item):

Test[int, int]  # (<class 'int'>, <class 'int'>)
Test[(int, int)]  # (<class 'int'>, <class 'int'>)
Test[tuple([int, int])]  # (<class 'int'>, <class 'int'>)

I don’t think that’s doing what you think. You called tuple on a list of int type objects, it gave you (int, int) (a tuple), which you then passed to __class_getitem__. So of course it printed the type of the tuple.

I’m not sure what you thought this was demonstrating, to be honest.

To clarify, instances of collections could be used as type annotations. That’s the only way in which this makes sense:

LIST = [int]
DICT = {str: int}
SET = {int}
TUPLE = (int, str)
def foo(bar: LIST, baz: DICT, qux: SET) -> TUPLE:

You need to use this to call it with a tuple as argument:

Test[(int, int),]  # ((<class 'int'>, <class 'int'>),)

Something like this (a generalisation of the behaviour of None and strings):

from types import GenericAlias

class Tuple(tuple):
    def __alias__(self):
        return type(self)[*self]

def alias(obj):
    print(obj if isinstance(obj, (type, GenericAlias)) else obj.__alias__())

alias(Tuple)  # <class '__main__.Tuple'>
alias(Tuple[int, str])  # __main__.Tuple[int, str]
alias(Tuple((int, str)))  # __main__.Tuple[int, str]

This has been suggested before. The reason this doesn’t really work is that it doesn’t compose well with pythons current typing syntax: (int, int) would be a tuple of two ints. tuple[tuple[int, int]] is currently a tuple containing a tuple of two ints. What would tuple[(int, int)] be? The former or the latter? Even if you find a consistent answer, it wouldn’t be obvious to human parsers, massively increasing the cost for the feature. And it is just a small convenience right now, so I don’t think that hurtle is anywhere near cleared.

Like this (as confusing as regular tuples right now):

int, int      or tuple[int, int]      -> tuple[int, int]
(int, int)    or tuple[(int, int)]    -> tuple[int, int]
(int, int),   or tuple[(int, int),]   -> tuple[tuple[int, int]]
((int, int),) or tuple[((int, int),)] -> tuple[tuple[int, int]]

Note: this is AFTER conversion to generic aliases.

1 Like

To repeat:

And certainly we’re far away from “it looks better” at this point.

In my opinion a linter should complain if you’re mixing the two. That’s just going to confuse people.
Use it when it makes sense.

Also conversion needs to support recursion. Tuple( would be written as (:

from types import GenericAlias

class Tuple(tuple):
    def __new__(cls, *args):
        return super().__new__(cls, args)
    def __alias__(self):
        return type(self)[tuple(map(alias, self))]

def alias(obj):
    return obj if isinstance(obj, (type, GenericAlias)) else obj.__alias__()

print(alias(Tuple))  # <class '__main__.Tuple'>
print(alias(Tuple[int, str]))  # __main__.Tuple[int, str]
print(alias(Tuple(int, str)))  # __main__.Tuple[int, str]
print(alias(Tuple[Tuple[int, str]]))  # __main__.Tuple[__main__.Tuple[int, str]]
print(alias(Tuple(Tuple(int, str),)))  # __main__.Tuple[__main__.Tuple[int, str]]

Or with custom types:

from typing import _GenericAlias, Literal

class Bool(int):
    def __new__(cls, value):
        return super().__new__(cls, bool(value))
    def __alias__(self):
        return Literal[self]

def alias(obj):
    return obj if isinstance(obj, (type, _GenericAlias)) else obj.__alias__()

TRUE = Bool(True)
print(alias(Literal[TRUE]))  # typing.Literal[1]
print(alias(TRUE))  # typing.Literal[1]

I think one of the main draws of Python has been that it looks “good”, as in more readable, more intuitive and with less noise, compared to other high-level languages, so a proposed syntax that potentially looks better than an existing one may very well be a valid argument for a change IMHO.

Past changes such as allowing tuple to replace typing.Tuple, class Class[T] to replace class Class(Generic[T]) and a type statement to replace TypeAlias, are all efforts mostly to help make Python’s typing system look better and less verbose.

I personally don’t have a problem with def go() -> int, str: just like I don’t with a valid assignment like types = int, str. I can understand it though when I see people preferring to always write types = (int, str) instead.

The existing syntax is comparatively verbose with explicit names required even for native data types. The proposed syntax does look more intuitive to me, aligning more to Rust.

However, I agree with the sentiment of other posters here that the inability for the proposed syntax to unambiguously compose with the existing syntax may be a crippling factor, since the ship has already sailed for Python’s typing system. The only way for this proposal to go forward is to reinvent a new typing system that does everything the current system can without involving __class_getitem__ at all.