Skip to content

Regression: false positive avoid redundant cast with literal union #19055

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

Open
JukkaL opened this issue May 8, 2025 · 5 comments
Open

Regression: false positive avoid redundant cast with literal union #19055

JukkaL opened this issue May 8, 2025 · 5 comments
Labels
bug mypy got something wrong priority-0-high

Comments

@JukkaL
Copy link
Collaborator

JukkaL commented May 8, 2025

This example started generating an error after #18588, which looks like a regression:

# mypy: warn-redundant-casts

from typing import Literal, cast

Foo = Literal["a", "b"]

class C:
    def __init__(self) -> None:
        self.x = cast(Foo, "a")  # error: Redundant cast to "Literal['a', 'b']"

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.

cc @asottile as the author of the PR

@JukkaL JukkaL added bug mypy got something wrong priority-0-high labels May 8, 2025
@sterliakov
Copy link
Collaborator

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.

@JukkaL
Copy link
Collaborator Author

JukkaL commented 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:

# mypy: warn-redundant-casts

from typing import cast

Foo = int | str

class C:
    def __init__(self) -> None:
        self.x = cast(Foo, "a")  # No error

Is there an inconsistency?

@sterliakov
Copy link
Collaborator

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.

@sterliakov
Copy link
Collaborator

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
Copy link
Collaborator

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

# mypy: warn-redundant-casts

from typing import cast, Literal

Foo = Literal['a', 'b']
val = cast(Foo, "a")

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong priority-0-high
Projects
None yet
Development

No branches or pull requests

2 participants