Not planned
Description
code:
from typing import TypeVar
class A:
pass
class B:
pass
class C:
pass
T = TypeVar("T", A, B, C)
def foo(o: T) -> None:
assert False
def bar(o: A | B) -> None:
return foo(o)
This fails: main.py:24: error: Value of type variable "T" of "foo" cannot be "A | B" [type-var]
In this case, where there is exactly one parameter with type T
, I would expect it to succeed: in the call foo(o)
, with o: A | B
, o
must be either A
(which is part of T
's range), or B
(likewise).
I understand that this would also fail and should fail:
def foo2(a: T, b: T) -> None:
assert False
def bar2(a: A | B, b: A | B) -> None:
return foo2(a, b)
Because a
could be A
and b
could be B
, which would violate the restriction in foo2
that they be the same type (whatever type they might be). But with foo
as written, no such mischief can arise.
Metadata
Metadata
Assignees
Labels
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
A5rocks commentedon May 23, 2025
This is intentional;
A | B
is a super type ofA
andB
, not necessarily one of them. I can't think of a counterexample with instances, so I think this is only matters because structural typing (?). With literals tryA = Literal[1, 2]
andB = Literal[3, 4]
, then passingx: Literal[1, 4]
tobar
.bwo commentedon May 24, 2025
tbh that doesn't make much sense to me either. Suppose you've got this (I realize the first is not the exact scenario you described):
This fails with the error
error: Value of type variable "S" of "baz" cannot be "Literal[1, 2, 3, 4]" [type-var]
(and likewise forspam
, with the error being thatS
cannot beLiteral[1, 4]
). But saying thats: S
means thats
can take on this range of values:1
,2
,3
,4
. This would again be significant with two arguments, because you might then meaningfully say that either they're both in(1, 2)
or they're both in(3, 4)
but you will never have one be1
and the other be4
. But with one argument, then yes,Literal[1, 2, 3, 4]
(orLiteral[1, 4]
) is not a subtype of eitherLiteral[1, 2]
orLiteral[3, 4]
, but any value of those types is guaranteed to be within the range, as can be seen by the fact that this version typechecks fine:(likewise the original
bar
works withisinstance
checks). This is just mechanically duplicating the code for the ranges of (types of) values it might take on. This even works with multiple arguments so long as both are properly derived fromo
, so that for instance this works:In fact it makes perfect sense that these would work out similarly, because when I have
x: Literal[1, 4]
as a value I wish to assign to a variable of typeS
, that's really no different from saying that I have anx: Literal[1] | Literal[4]
, and just as myo: A | B
is either anA
(hence it is one of the types aT
can be, or aB
(hence again one of the types aT
can be), myx
is either a1
(hence one of the types anS
can be), or a4
(hence the other).I certainly agree that
Literal[1] | Literal[4]
is not a subtype of eitherLiteral[1,2]
or ofLiteral[3,4]
. But I think that it's not out of the question to ask mypy to do this analysis: each of the types it could be is a subtype of one or the other, and since there is only one value of the type in play no weird nonsense of thestr + bytes
stuff can happen.A5rocks commentedon May 24, 2025
OK I see I actually agree with you now, I didn't think through this enough. But: