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
Regression: false positive avoid redundant cast with literal union #19055
I haven't looked into this in detail, but it seems possible that we infer a union type for "a" in a literal union context, which doesn't look right. If this is a correct hypothesis, #18588 wouldn't actually be the root cause, and it would just be exposing a pre-existing issue.
I'd say this is indeed a redundant cast as self.x doesn't have a type prior to this assignment. If you have x: SomethingIncompatible annotation, cast won't be marked redundant. How problematic is this use case for you, especially given that the fix is as trivial as self.x: Foo = 'a'?
I suspect the logic goes as follows: during the first pass we have an unknown type and infer x: Foo, then during the second pass the assignment looks like self.x: Foo = cast(Foo, "a") which is clearly a redundant cast.
Indeed, that's a bit inconsistent: we only detect unnecessary casting to Literal types in this scenario (same if you annotate x: Foo directly in the body of C). That happens because we do infer literal types when in literal context, but never infer other union types when one of the union members suffices. This discrepancy isn't a new regression, it just happens to surface with --warn-redundant-casts is enabled.
I'd argue that this aspect of new mypy behavior is correct: the cast is redundant. So the actual bug is that we fail to report cast(int | str, 'a')? Given that --warn-redundant-casts is more of cosmetic lint (violation mean that the user might have missed something, not that a program is ill-formed from typing POV), I think detecting some new cases is better than not detecting, even if somewhat inconsistently.
When we encounter cast(Foo, "a"), we check is_same_type(a, b), where a is an Instance(str, last_known_value="a") and b is a union of LiteralTypes (expansion of Literal['a', 'b']). is_same_type is almost equivalent to a <: b && b <: a. Now, a <: b is trivially true: we remember last_known_value matching one of the literals. b <: a is also trivially true: a is just some string, any literal is assignable to it.
We're a missing a bit of shared context: if a <: b needs casting to a literal and b <: a works as-is, is_same_type(a, b) should be False. This rule doesn't seem to generalize well: what if we do the same to tuples, for example, and need to interpret different elements as literals?
One way to tackle that would be to modify elements interpreted as Literal to return True from some part of comparison, but that means rewriting is_proper_subtype from scratch and maintaining two copies of it (because it's hot, so doing that for non-paired is_subtype checks would be prohibitively slow).
We can also try to expand is_same_type special cases, that's cheaper, but rather complicated: I'm unable to quickly formulate the rejection rule involving "instances of literal-compatible types with last_known_value set" and "literal types and unions thereof" that clicks as correct.
So this might indeed be a bug, but I doubt it warrants the P0 label. It's been like that for a very long time and caused no other tickets I'm aware of, and I don't think it has significant adverse impact on type checking correctness.
Activity
sterliakov commentedon May 8, 2025
I'd say this is indeed a redundant cast as
self.xdoesn't have a type prior to this assignment. If you havex: SomethingIncompatibleannotation, cast won't be marked redundant. How problematic is this use case for you, especially given that the fix is as trivial asself.x: Foo = 'a'?I suspect the logic goes as follows: during the first pass we have an unknown type and infer
x: Foo, then during the second pass the assignment looks likeself.x: Foo = cast(Foo, "a")which is clearly a redundant cast.JukkaL commentedon May 8, 2025
The reason this looked like a regression is that I've only seen this happen with literal types. This doesn't generate an error:
Is there an inconsistency?
sterliakov commentedon May 8, 2025
Indeed, that's a bit inconsistent: we only detect unnecessary casting to Literal types in this scenario (same if you annotate
x: Foodirectly in the body ofC). That happens because we do infer literal types when in literal context, but never infer other union types when one of the union members suffices. This discrepancy isn't a new regression, it just happens to surface with--warn-redundant-castsis enabled.I'd argue that this aspect of new mypy behavior is correct: the cast is redundant. So the actual bug is that we fail to report
cast(int | str, 'a')? Given that--warn-redundant-castsis more of cosmetic lint (violation mean that the user might have missed something, not that a program is ill-formed from typing POV), I think detecting some new cases is better than not detecting, even if somewhat inconsistently.sterliakov commentedon May 8, 2025
Hm, no, my bad, this still looks weird. It's a positive change, but I don't understand why it actually happens.
Why do we infer a union of literals somewhere when its subset would suffice?
sterliakov commentedon May 9, 2025
The issue you reported was only exposed by #18588 and not caused by it. And my initial analysis was also completely wrong, the minimal reproducer is
When we encounter
cast(Foo, "a"), we checkis_same_type(a, b), whereais anInstance(str, last_known_value="a")andbis a union ofLiteralTypes (expansion ofLiteral['a', 'b']).is_same_typeis almost equivalent toa <: b && b <: a. Now,a <: bis trivially true: we rememberlast_known_valuematching one of the literals.b <: ais also trivially true:ais just some string, any literal is assignable to it.We're a missing a bit of shared context: if
a <: bneeds casting to a literal andb <: aworks as-is,is_same_type(a, b)should be False. This rule doesn't seem to generalize well: what if we do the same to tuples, for example, and need to interpret different elements as literals?is_proper_subtypefrom scratch and maintaining two copies of it (because it's hot, so doing that for non-pairedis_subtypechecks would be prohibitively slow).is_same_typespecial cases, that's cheaper, but rather complicated: I'm unable to quickly formulate the rejection rule involving "instances of literal-compatible types withlast_known_valueset" and "literal types and unions thereof" that clicks as correct.So this might indeed be a bug, but I doubt it warrants the P0 label. It's been like that for a very long time and caused no other tickets I'm aware of, and I don't think it has significant adverse impact on type checking correctness.
JukkaL commentedon May 12, 2025
Thanks for the analysis! Removed the P0 label, since even though this is a regression, it seems pretty minor and related to a pre-existing issue.