fixed Incorrect type of enum in 'if' clause #20234
Fixes #20234 --- Correct Enum Literal Narrowing for in Operator with Tuples
Problem
When using the in operator with tuples containing enum members, mypy
failed to narrow the left-hand side to the correct enum literal type.
Before this fix:
if pizza in (Pizza.MARGHERITA,):
reveal_type(pizza)
# Revealed: Pizza
# Expected: Literal[Pizza.MARGHERITA]
Mypy treated the tuple contents as regular Instance types and did not
extract the literal enum values, so narrowing never happened.
Solution
Enhanced comparison_type_narrowing_helper to detect tuples containing
enum literals and correctly narrow the type based on the enum values
inside the tuple.
Key Points:
- Enum values inside tuples appear as Instance types, not
LiteralType. - Their literal identity is available via
last_known_value. - The fix extracts these literal values, builds a
Unionof enum literals, and applies narrowing on the true branch of theinexpression.
Implementation Details
File: mypy/checker.py
Function: comparison_type_narrowing_helper
Added enum-specific logic immediately after the existing None-removal
narrowing for the in operator:
- Extract literal enum values from the tuple's items\
- Construct a
Union[Literal[Enum.X], Literal[Enum.Y], ...]\ - Use that union as the narrowed type for the LHS\
- Supports both
inandnot in
Tests & Behavior
Correctly handled:
- Single enum in tuple
python if op in (Op.A,): reveal_type(op) # Literal[Op.A] - Multiple enums
python if op in (Op.A, Op.B): reveal_type(op) # Literal[Op.A] | Literal[Op.B] - Works with
not in - Preserves existing None-removal behavior
Example
from enum import Enum
class Op(Enum):
A = "a"
B = "b"
def process(op: Op) -> None:
if op in (Op.A,):
reveal_type(op) # Literal[Op.A]
return
if op is Op.B:
reveal_type(op) # Literal[Op.B]
return
Diff from mypy_primer, showing the effect of this PR on open source code:
psycopg (https://github.com/psycopg/psycopg)
+ tests/pq/test_pgconn.py:508: error: Non-overlapping equality check (left operand type: "Literal[ConnStatus.STARTED, ConnStatus.MADE]", right operand type: "Literal[ConnStatus.OK]") [comparison-overlap]
prefect (https://github.com/PrefectHQ/prefect)
+ src/prefect/flows.py:3103: error: Redundant cast to "str" [redundant-cast]
+ src/prefect/flows.py:3116: error: Redundant cast to "str" [redundant-cast]
core (https://github.com/home-assistant/core)
- homeassistant/components/shelly/event.py:305: error: Argument 1 to "_trigger_event" of "EventEntity" has incompatible type "Any | None"; expected "str" [arg-type]
pytest (https://github.com/pytest-dev/pytest)
+ src/_pytest/config/argparsing.py:247: error: Statement is unreachable [unreachable]
steam.py (https://github.com/Gobot1234/steam.py)
- steam/ext/csgo/state.py:180: error: Argument "slot" to "Sticker" has incompatible type "int | None"; expected "Literal[0, 1, 2, 3, 4, 5] | None" [arg-type]
+ steam/ext/csgo/state.py:180: error: Argument "slot" to "Sticker" has incompatible type "int"; expected "Literal[0, 1, 2, 3, 4, 5] | None" [arg-type]
discord.py (https://github.com/Rapptz/discord.py)
+ discord/app_commands/models.py:900: error: Unused "type: ignore" comment [unused-ignore]
+ discord/threads.py:286: error: Unused "type: ignore" comment [unused-ignore]
xarray (https://github.com/pydata/xarray)
+ xarray/backends/writers.py:841: error: Unused "type: ignore" comment [unused-ignore]
Please don't use an LLM to generate the description or responses! We're fine with extremely short or curt text (I've made so many PRs where the description was just a "fixes [link]"...), and making it longer just wastes time.
Additionally, could you add test cases to the tests? Mypy has an extensive unit test suite. (I know you mentioned them in the PR description, but really they belong as a test case)
Thanks for the info, i thought the LLM prompt was so detailed, i'll add a shorter PR next time and fix the tests:)