Recognition of tuples under get_origin

Context

When working with function return annotations, I encountered a situation where (str, str) was used instead of Tuple[str, str]. This syntax is valid Python for creating a tuple of types, but it’s not recognized by typing.get_origin in the same way as Tuple[str, str] or tuple[str, str].

Current Behavior

from typing import get_origin, get_args

def func() -> (str, str):
    pass

print(get_origin(func.__annotations__['return']))  # None
print(get_args(func.__annotations__['return']))    # ()

Desired Behavior

Ideally, (str, str) would be recognized similarly to Tuple[str, str]:

from typing import get_origin, get_args, Tuple

def func() -> Tuple[str, str]:
    pass

print(get_origin(func.__annotations__['return']))  # <class 'tuple'>
print(get_args(func.__annotations__['return']))    # (<class 'str'>, <class 'str'>)

Discussion

As someone who’s has not been intimately familiar with python’s typing development I would be keen to learn any background/ if this is intentional. If not I’d be happy to look into what changes would be required for a PR.

I’m also keen to validate my understanding for correctness with this community, as I currently see it this change would have:

Benefits

  1. Alignment in validity between type annotation declarations and typing utils
  2. Better support for static type checking tools (my use case)
  3. More intuitive behavior for users

Drawbacks

  1. Backward compatibility concerns
  2. Crossing the boundary between typing declarations and standard declarations

Questions for Discussion

  1. Is my current understanding correct?
  2. Is this a change worth pursuing?
  3. What potential issues might this cause?
  4. Are there alternative solutions to consider?

Tuple expressions are not currently allowed in type expressions. For details, refer to the Python typing spec. Refer to this section for details.

In addition to the drawbacks you’ve already listed, I’ll add:
3. Backward compatibility with static type checkers, language servers, linters, etc. that honor the current static type spec. (I’m assuming here that your “backward compatibility” point is referring to runtime concerns.)
4. Exposes a second, redundant way to “spell” a tuple type in a type expression. Adding redundant ways to do something can lead to confusion, so it should be done cautiously.
5. Eliminates tuple expressions for future use in static typing for other purposes.

Perhaps most importantly:
6. Introduces an ambiguity in PEP 695 generics syntax between a type parameter with an upper bound versus a type parameter with value constraints. For details, refer to this section and this section of PEP 695.

What you’re suggesting has a lot of downsides, and I don’t think it solves any real problem. If you want to spell a tuple type in a type expression, you can already do so today using the expression tuple[str, str].

Past discussions for reference:

I think there was also another longer discussion, but I couldn’t find it. Generally this is a bad idea because subscript syntax contains implicit parentheses for brevity, so T[A, B] is equivalent to T[(A, B)][1].


  1. Technically parantheses aren’t a required part of any tuple, they’re only there to resolves ambiguities, what makes a tuple a tuple is , the only exception to that rule is the empty tuple syntax sugar () ↩︎

4 Likes