You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
meet_types gives unexpected results when meeting literal and Instance #19560
The internal mypy.meet.meet_types function gives unexpected types, when one argument is a LiteralType, and the other is a Instance whose last_known_value is the other argument:
The predicted result is an Instance type with last_known_value=Literal["max"], but I think the result should be the LiteralType, since the former can be converted to plain str type later on, which would then be incorrect.
Similar issues are also present in the mypy.solve.solve_constraint function:
This is not #19279, I only touched join there, and with the opposite outcome: join(Instance(lkv=L), L) for some literal L returns L. Probably smth similar is needed in meet, but I don't exactly understand why: what's the actual problem caused by using an Instance with l.k.v. in this case?
can be converted to plain str type later on
yep, and so can Literal - it has a fallback type of Instance(builtins.str). Does it cause any externally observable issues? AFAIC the only problem of using an Instance with l.k.v. instead of Literal is that it may sometimes not be interpreted as a literal, breaking the inference, but not the promotion ability.
Well, I have been working on a PR that reworks inference for generic callables to fix a whole range of bugs. (Here's an earlier draft: #19399; I may push a more recent version soon)
Essentially, instead of the current two stage approach of first considering the outer context and then the inner, we do both simultaneously. Here's a case where this goes wrong due to this issue:
And this is the correct type. Later conversion (erasing last known value) should not have happened in literal context. If it really makes other places much easier, maybe it's OK to try to change this, but in general Literal types should only be inferred in literal context (for meet/join - when at least one of the operands is a Literal type). I'd probably try to understand the reason for last known value erasure first, though.
but in general Literal types should only be inferred in literal context
What exactly do you mean by this? In my example, the variable is annotated with Mapping[str, Literal["sum", "mean", "max", "min"]]. Is this not a literal context?
(for meet/join - when at least one of the operands is a Literal type).
Sorry for being naive about the terminology here, but I would assume that a Literal context is whenever there appear type constraints referencing actual LiteralType types (rather than constraints containing just Literal? types)
Sorry, no, it was a rather general remark, it's fine to infer Literals here! My only point was that joining/meeting several Instances with l.k.v., for example, should never produce a Literal type - we try hard to never annoy people by inferring literals that prevent sound operations later.
And still the why is interesting, I don't see any obvious l.k.v. erasure between constraint solving and dict call checking.
My only point was that joining/meeting several Instances with l.k.v.,
Uhm, in the example only the lower bound is an instance, the upper bound is an actual LiteralType. What happens here is that within solve_one it exits here:
It considers Literal[T]? a subtype of Literal[T] and so returns it as-is.
Maybe the problem here is related to the assignability vs subtype discussion? Literal[T]? should be assignable to Literal[T]. But should it really be a considered a subtype? Possibly this thing is in a similar category as Any, where it is a gradual subtype of Literal[T] and a gradual subtype of str.
@sterliakov There were actually more weird things I noticed with Literal? types which prompted me to check most of the basic operations (meet, join, union, etc. for them). I opened #19625 with my discoveries and proposed changes.
Activity
A5rocks commentedon Aug 1, 2025
cc @sterliakov (this behavior was added in #19279)
sterliakov commentedon Aug 1, 2025
This is not #19279, I only touched join there, and with the opposite outcome:
join(Instance(lkv=L), L)for some literal L returnsL. Probably smth similar is needed in meet, but I don't exactly understand why: what's the actual problem caused by using an Instance with l.k.v. in this case?yep, and so can Literal - it has a fallback type of
Instance(builtins.str). Does it cause any externally observable issues? AFAIC the only problem of using an Instance with l.k.v. instead of Literal is that it may sometimes not be interpreted as a literal, breaking the inference, but not the promotion ability.A5rocks commentedon Aug 1, 2025
Oh sorry I just barely remembered "hey I think I saw something about this at some point" and found the PR. I should have taken a more thorough look!
sterliakov commentedon Aug 1, 2025
Nah, never mind - it was a good idea to ping me here anyway:)
randolf-scholz commentedon Aug 1, 2025
Well, I have been working on a PR that reworks inference for generic callables to fix a whole range of bugs. (Here's an earlier draft: #19399; I may push a more recent version soon)
Essentially, instead of the current two stage approach of first considering the outer context and then the inner, we do both simultaneously. Here's a case where this goes wrong due to this issue:
Here we consider the callable
def [K, V](...) -> dict[K, V].The outer constraints are:
dict[K, V] <: Mapping[str, Literal["sum", "mean", "max", "min"]]which translates to[1 <: builtins.str, 1 :> builtins.str, 2 <: Literal['sum'] | Literal['mean'] | Literal['max'] | Literal['min']]and the inner constraints, based on the provided arguments are:
[1 :> Literal['x']?, 2 :> Literal['sum']?]If we jointly solve these, we get
[builtins.str, Literal['sum']?]Which somewhere later on gets coerced to
[builtins.str, builtins.str], causing a[assignment]error.Instead, if the solver produced
[builtins.str, Literal['sum']], everything would be fine.sterliakov commentedon Aug 1, 2025
And this is the correct type. Later conversion (erasing last known value) should not have happened in literal context. If it really makes other places much easier, maybe it's OK to try to change this, but in general
Literaltypes should only be inferred in literal context (for meet/join - when at least one of the operands is a Literal type). I'd probably try to understand the reason for last known value erasure first, though.randolf-scholz commentedon Aug 1, 2025
What exactly do you mean by this? In my example, the variable is annotated with
Mapping[str, Literal["sum", "mean", "max", "min"]]. Is this not a literal context?Which is the case for the example in #19560 (comment)
randolf-scholz commentedon Aug 1, 2025
Sorry for being naive about the terminology here, but I would assume that a Literal context is whenever there appear type constraints referencing actual
LiteralTypetypes (rather than constraints containing justLiteral?types)sterliakov commentedon Aug 1, 2025
Sorry, no, it was a rather general remark, it's fine to infer Literals here! My only point was that joining/meeting several
Instances with l.k.v., for example, should never produce a Literal type - we try hard to never annoy people by inferring literals that prevent sound operations later.And still the why is interesting, I don't see any obvious l.k.v. erasure between constraint solving and dict call checking.
randolf-scholz commentedon Aug 1, 2025
Uhm, in the example only the lower bound is an instance, the upper bound is an actual
LiteralType. What happens here is that withinsolve_oneit exits here:mypy/mypy/solve.py
Lines 321 to 322 in 07d4a1b
Because it determines that
Literal["sum"]?is a subtype ofLiteral["sum", "max", "min", "mean"].A similar code is present in
meet_typewhich causes the same behavior there:mypy/mypy/meet.py
Lines 97 to 101 in 07d4a1b
It considers
Literal[T]?a subtype ofLiteral[T]and so returns it as-is.Maybe the problem here is related to the assignability vs subtype discussion?
Literal[T]?should be assignable toLiteral[T]. But should it really be a considered a subtype? Possibly this thing is in a similar category asAny, where it is a gradual subtype ofLiteral[T]and a gradual subtype ofstr.randolf-scholz commentedon Aug 1, 2025
Like to me, it would make sense (interpreting
meetas intersection), andLiteral?as the sort of gradual type mentioned above, if:meet(Literal["sum"]?, str) -> Literal["sum"]?meet(Literal["sum"]?, Literal["sum"]) -> Literal["sum"]meet(Literal["sum"]?, Literal["max"]) -> Neversterliakov commentedon Aug 1, 2025
Yes, looks reasonable! I agree current state is probably inconvenient. Let's try and see the tests and primer...
randolf-scholz commentedon Aug 7, 2025
@sterliakov Primer looks decent in a first draft: #19605
randolf-scholz commentedon Aug 9, 2025
@sterliakov There were actually more weird things I noticed with
Literal?types which prompted me to check most of the basic operations (meet, join, union, etc. for them). I opened #19625 with my discoveries and proposed changes.blocker: python#19560