Skip to content

bpo-43224: Forbid TypeVar substitution with Unpack #32031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,20 @@ class G(Generic[Unpack[Ts]]): pass
self.assertEqual(E[float, str, int, bytes],
Tuple[List[float], A[str, int], List[bytes]])

def test_bad_var_substitution(self):
Ts = TypeVarTuple('Ts')
T = TypeVar('T')
T2 = TypeVar('T2')
class G(Generic[Unpack[Ts]]): pass

for A in G, Tuple:
B = A[T, Unpack[Ts], str, T2]
with self.assertRaises(TypeError):
B[int, Unpack[Ts]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remind me where PEP 646 forbids this? Assuming Unpack[Ts] === *Ts, it seems this is forbidding the following:

class G(Generic[*Ts]): ...
B = G[T, *Ts, str, T2]
X = B[int, *Ts]  # <-- TypeError here

What is wrong with X that isn't wrong with B?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rewrite it as B[int, *Ts2], so we could unambiguously refer to variables.

AFAIK PEP 646 does not cover such case. It does not specify the result.

But what can be the result of substitution B[int, *Ts2]? T is substituted with int, T2 should be substituted with the last item of *Ts2, and *T should be substituted with all but the last items of *Ts2. But we cannot express this, because Ts2 is a variable. If we substitute *Ts2 with an empty sequence of types (X[()]), there would not be a value for T2.

I think that it is better to make it an error:

def f(a: tuple[*Ts2]) -> B[int, *Ts2]: ...

You should write

def f(a: tuple[*Ts2, T3]) -> B[int, *Ts2, T3]: ...

to make it having some sense.

There is a workaround of this problem. It may even make the code more clear. You can see, that a should be a tuple containing at least 1 item. T2 in B will be substituted with the type of the last item, and *Ts in B will be substituted with the rest of types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mrahtz what do you think of this case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the prod, Jelle. I agree with Serhiy - looks like this is one of the cases we should forbid at runtime.

C = A[T, Unpack[Ts], str, T2]
with self.assertRaises(TypeError):
C[int, Unpack[Ts], Unpack[Ts]]

def test_repr_is_correct(self):
Ts = TypeVarTuple('Ts')
self.assertEqual(repr(Ts), 'Ts')
Expand Down
2 changes: 2 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,8 @@ def __init__(self, name, *constraints, bound=None,
def __typing_subst__(self, arg):
msg = "Parameters to generic types must be types."
arg = _type_check(arg, msg, is_argument=True)
if (isinstance(arg, _GenericAlias) and arg.__origin__ is Unpack):
raise TypeError(f"{arg} is not valid as type argument")
return arg


Expand Down