There are two empty typing.Tuple. They have the same repr but are not equal. >>> from typing import * >>> t1 = Tuple[()] >>> t2 = t1.copy_with(()) >>> t1 typing.Tuple[()] >>> t2 typing.Tuple[()] >>> t1 == t2 False >>> t1.__args__ ((),) >>> t2.__args__ () The only differences is that one has empty __args__, while other has __args__ containing an empty tuple. There is a code purposed to make __args__ containing an empty tuple in this case. What is the purpose? It is not pure theoretical question. This affects unpacked TypeVarTuple substitution. With natural implementation Tuple[Unpack[Ts]][()] is not equal to Tuple[()] and I still have not figured which and where code should be added to handle this special case. It would be easier if such special case did not exist. Built-in tuple does not have a special case: >>> tuple[()].__args__ ()
Alas, I have no idea. I don't even recall what copy_with() is for (it was apparently introduced in 3.7). Possibly vopy_with() is wrong here? I imaging some of this has to do with the special casing needed so that repr() of an empty Tuple type doesn't print "Tuple[]" (which IIRC it did, long ago).
I tried out 3.11 on my pyanalyze type checker and got some failures because of this change, because my previous trick for distinguishing between Tuple and Tuple[()] failed. 3.10: >>> from typing import get_args, Tuple >>> get_args(Tuple[()]) ((),) >>> get_args(Tuple) () 3.11: >>> from typing import get_args, Tuple >>> get_args(Tuple[()]) () >>> get_args(Tuple) () However, the new behavior is more consistent: get_args(tuple[()]) always returned (). It's also easy enough to work around (just check `... is Tuple`). I'll put a note in the What's New for 3.11 about this change.