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
Are you reporting a bug, or opening a feature request?
Asking a question. I'd like to know if there is a standard idiom for enforcing exhaustive matching of an Enum / Literal type.
Please insert below the code you are checking with mypy
Suppose there are three types of pets: cats, dogs and iguanas. I want to write a function that makes the appropriate pet sound, depending on the type of pet. I'd also like to make sure that I've caught every possible type of pet. Here was my first attempt, using the NoReturn trick mentioned by @bluetechhere. It looks like this works for Unions, but not for Enums / Literals? Apologies if this has been covered before.
But with enums and literals it will probably always give you a false positive.
The exhaustive checks are not supported yet for enums and literals. But there are plans to add them, see also #5935. I am not sure about the timeline however, @Michael0x2a will you have time to work on this?
FWIW a simple Enum example seems to work for me on mypy 0.740:
class MobileOperator(enum.Enum):
ORANGE = "ORANGE"
FREE = "FREE"
EXPRESSO = "EXPRESSO"
FOO = "FOO"
def _assert_never(x: NoReturn) -> NoReturn:
assert False, "Unhandled type: {}".format(type(x).__name__)
def _mobile_operator_to_url(operator: MobileOperator) -> str:
if operator is MobileOperator.ORANGE:
return "/api/orange/"
elif operator is MobileOperator.FREE:
return "/api/free/izi"
elif operator is MobileOperator.EXPRESSO:
return "/api/expresso/yakalma"
else:
_assert_never(operator)
which gives the type error Argument 1 to "_assert_never" has incompatible type "Literal[MobileOperator.FOO]"; expected "NoReturn". Removing FOO from the enum fixes the error.
Reacted by Eero Ruohola, Matt Traudt, Langston Barrett, Mathieu PATUREL and Jørgen Wilhelmsen
While the Python docs say that members can be compared by identity, the docs also mentioned that equality operators are defined.
I think as long as the Enum doesn't use a mixin, like class Foo(str, Enum): ... or is an IntEnum, then comparing via equality should work for refinement since only other enum members will be equal.
I was looking into this more and it seems that not narrowing based on == is intended behavior to prevent false positives.
fromenumimportEnumfromtypingimportLiteralclassFoo(Enum):
a="a"b="b"x: FooA: Literal[Foo.a]
ifx==A:
reveal_type(x) # note: Revealed type is 'Literal[main.Foo.a]'else:
reveal_type(x) # note: Revealed type is 'Literal[main.Foo.b]'
this doesn't:
ifx==Foo.a:
reveal_type(x) # note: Revealed type is 'main.Foo'else:
reveal_type(x) # note: Revealed type is 'main.Foo'
There are some test cases explaining the decision around narrowing:
Maybe narrowing enums via equality could be put behind another flag to prevent breaking existing code?
Here's a diff to change the narrowing, but it does require changing some tests:
mypy.diff
diff --git a/mypy/checker.py b/mypy/checker.py
index fce7e7d7..cf25bdb2 100644
--- a/mypy/checker.py+++ b/mypy/checker.py@@ -4083,7 +4083,7 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
should_narrow_by_identity = True
else:
def is_exactly_literal_type(t: Type) -> bool:
- return isinstance(get_proper_type(t), LiteralType)+ return isinstance(coerce_to_literal(get_proper_type(t)), LiteralType)
def has_no_custom_eq_checks(t: Type) -> bool:
return (not custom_special_method(t, '__eq__', check_all=False)
@@ -4451,7 +4451,7 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
singleton_index = possible_target_indices[-1]
enum_name = None
- target = get_proper_type(target)+ target = coerce_to_literal(get_proper_type(target))
if isinstance(target, LiteralType) and target.is_enum_literal():
enum_name = target.fallback.type.fullname
diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test
index 0da3217b..de6c606e 100644
--- a/test-data/unit/check-enum.test+++ b/test-data/unit/check-enum.test@@ -855,6 +855,24 @@ else:
reveal_type(z) # No output: this branch is unreachable
[builtins fixtures/bool.pyi]
+[case testEnumReachabilityChecksEquality]+from enum import Enum+from typing_extensions import Literal++class Foo(Enum):+ A = 1+ B = 2+ C = 3++x: Foo++if x == Foo.A:+ reveal_type(x) # N: Revealed type is 'Literal[__main__.Foo.A]'+else:+ reveal_type(x) # N: Revealed type is 'Union[Literal[__main__.Foo.B], Literal[__main__.Foo.C]]'++[builtins fixtures/bool.pyi]+
[case testEnumReachabilityNoNarrowingForUnionMessiness]
from enum import Enum
from typing_extensions import Literal
diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test
index 8eb6f643..5eb356c6 100644
--- a/test-data/unit/check-narrowing.test+++ b/test-data/unit/check-narrowing.test@@ -688,12 +688,12 @@ def test1(switch: FlipFlopEnum) -> None:
# approach and avoid narrowing anything here.
assert switch.state == State.A
- reveal_type(switch.state) # N: Revealed type is '__main__.State'+ reveal_type(switch.state) # N: Revealed type is 'Literal[__main__.State.A]'- switch.mutate()+ switch.mutate() - assert switch.state == State.B- reveal_type(switch.state) # N: Revealed type is '__main__.State'+ assert switch.state == State.B # E: Non-overlapping equality check (left operand type: "Literal[State.A]", right operand type: "Literal[State.B]")+ reveal_type(switch.state) # E: Statement is unreachable
def test2(switch: FlipFlopEnum) -> None:
# So strictly speaking, we ought to do the same thing with 'is' comparisons
@@ -713,12 +713,12 @@ def test3(switch: FlipFlopStr) -> None:
# This is the same thing as 'test1', except we try using str literals.
assert switch.state == "state-1"
- reveal_type(switch.state) # N: Revealed type is 'builtins.str'+ reveal_type(switch.state) # N: Revealed type is 'Literal['state-1']'
switch.mutate()
- assert switch.state == "state-2"- reveal_type(switch.state) # N: Revealed type is 'builtins.str'+ assert switch.state == "state-2" # E: Non-overlapping equality check (left operand type: "Literal['state-1']", right operand type: "Literal['state-2']")+ reveal_type(switch.state) # E: Statement is unreachable
[builtins fixtures/primitives.pyi]
[case testNarrowingEqualityRequiresExplicitStrLiteral]
@@ -733,13 +733,13 @@ A_literal: Literal["A"]
# why more precise inference here is problematic.
x_str: str
if x_str == "A":
- reveal_type(x_str) # N: Revealed type is 'builtins.str'+ reveal_type(x_str) # N: Revealed type is 'Literal['A']'
else:
reveal_type(x_str) # N: Revealed type is 'builtins.str'
reveal_type(x_str) # N: Revealed type is 'builtins.str'
if x_str == A_final:
- reveal_type(x_str) # N: Revealed type is 'builtins.str'+ reveal_type(x_str) # N: Revealed type is 'Literal['A']'
else:
reveal_type(x_str) # N: Revealed type is 'builtins.str'
reveal_type(x_str) # N: Revealed type is 'builtins.str'
@@ -784,15 +784,15 @@ A_literal: Literal[Foo.A]
# testNarrowingEqualityFlipFlop for more on why we can't narrow here.
x1: Foo
if x1 == Foo.A:
- reveal_type(x1) # N: Revealed type is '__main__.Foo'+ reveal_type(x1) # N: Revealed type is 'Literal[__main__.Foo.A]'
else:
- reveal_type(x1) # N: Revealed type is '__main__.Foo'+ reveal_type(x1) # N: Revealed type is 'Literal[__main__.Foo.B]'
x2: Foo
if x2 == A_final:
- reveal_type(x2) # N: Revealed type is '__main__.Foo'+ reveal_type(x2) # N: Revealed type is 'Literal[__main__.Foo.A]'
else:
- reveal_type(x2) # N: Revealed type is '__main__.Foo'+ reveal_type(x2) # N: Revealed type is 'Literal[__main__.Foo.B]'
# But we let this narrow since there's an explicit literal in the RHS.
x3: Foo
diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test
index 74a27093..6771bb38 100644
--- a/test-data/unit/check-optional.test+++ b/test-data/unit/check-optional.test@@ -487,7 +487,7 @@ foo([f]) # E: List item 0 has incompatible type "Callable[[], int]"; expected "
from typing import Optional
x = '' # type: Optional[str]
if x == '<string>':
- reveal_type(x) # N: Revealed type is 'builtins.str'+ reveal_type(x) # N: Revealed type is 'Literal['<string>']'
else:
reveal_type(x) # N: Revealed type is 'Union[builtins.str, None]'
if x is '<string>':
@@ -500,7 +500,7 @@ else:
from typing import Union
x = '' # type: Union[str, int, None]
if x == '<string>':
- reveal_type(x) # N: Revealed type is 'Union[builtins.str, builtins.int]'+ reveal_type(x) # N: Revealed type is 'Literal['<string>']'
else:
reveal_type(x) # N: Revealed type is 'Union[builtins.str, builtins.int, None]'
if x is '<string>':
@@ -526,7 +526,7 @@ else:
from typing import Optional
x = '' # type: Optional[str]
if x == 0:
- reveal_type(x) # N: Revealed type is 'Union[builtins.str, None]'+ reveal_type(x)
else:
reveal_type(x) # N: Revealed type is 'Union[builtins.str, None]'
if x is 0:
@@ -554,8 +554,8 @@ from typing import Optional
x: Optional[int]
y: Optional[int]
if x == y == 1:
- reveal_type(x) # N: Revealed type is 'builtins.int'- reveal_type(y) # N: Revealed type is 'builtins.int'+ reveal_type(x) # N: Revealed type is 'Literal[1]'+ reveal_type(y) # N: Revealed type is 'Literal[1]'
else:
reveal_type(x) # N: Revealed type is 'Union[builtins.int, None]'
reveal_type(y) # N: Revealed type is 'Union[builtins.int, None]'
Is the lack of IntEnum narrowing/exhaustiveness in mypy well-defined behavior? I ask because Pyright/VS Code are able to narrow the signal.HandlersIntEnum, for example, based on integer equality comparisons:
i: signal.Handlers = signal.Handlers.SIG_IGN
if i == signal.Handlers.SIG_IGN:
reveal_type(i) # Literal[Handlers.SIG_IGN] in pyright, Handlers in mypy
elif i == signal.Handlers.SIG_DFL:
reveal_type(i) # Literal[Handlers.SIG_DFL] in pyright, Handlers in mypy
else:
assert_never(i) # fails in mypy
Interestingly I can compare the integers with is (which I think would run because a small range of cpython integers are backed by a single location in memory per integer IIRC?) and get mypy to pass, but generally this should be unsafe at runtime:
if i is signal.Handlers.SIG_IGN:
reveal_type(i)
elif i is signal.Handlers.SIG_DFL:
reveal_type(i)
else:
assert_never(i)
Activity
ilevkivskyi commentedon Feb 9, 2019
The
assert_never()signature proposed in the issue you reference is a bit different:But with enums and literals it will probably always give you a false positive.
The exhaustive checks are not supported yet for enums and literals. But there are plans to add them, see also #5935. I am not sure about the timeline however, @Michael0x2a will you have time to work on this?
Generalize reachability checks to support enums
ilevkivskyi commentedon Jul 6, 2019
Note that #4223 gives an example where this can cause a false positive
Missing return statement.Generalize reachability checks to support enums (#7000)
davidshepherd7 commentedon Dec 2, 2019
FWIW a simple Enum example seems to work for me on mypy 0.740:
which gives the type error
Argument 1 to "_assert_never" has incompatible type "Literal[MobileOperator.FOO]"; expected "NoReturn". Removing FOO from the enum fixes the error.ezyang commentedon Aug 5, 2020
And on mypy 0.780 it seems Literal is working too:
correctly errors:
ezyang commentedon Aug 14, 2020
Note that you have to use
isto test enum equality and not==sbdchd commentedon Dec 18, 2020
While the Python docs say that members can be compared by identity, the docs also mentioned that equality operators are defined.
I think as long as the Enum doesn't use a mixin, like
class Foo(str, Enum): ...or is anIntEnum, then comparing via equality should work for refinement since only other enum members will be equal.https://docs.python.org/3.8/library/enum.html#comparisons
sbdchd commentedon Jan 29, 2021
I was looking into this more and it seems that not narrowing based on
==is intended behavior to prevent false positives.this doesn't:
There are some test cases explaining the decision around narrowing:
mypy/test-data/unit/check-narrowing.test
Lines 783 to 802 in 4d5a1bc
mypy/test-data/unit/check-narrowing.test
Lines 681 to 710 in 4d5a1bc
And a related commit: 3dce3fd
Maybe narrowing enums via equality could be put behind another flag to prevent breaking existing code?
Here's a diff to change the narrowing, but it does require changing some tests:
mypy.diff
ref(bot): use exhaustiveness checking via NoReturn type
ref(bot): use exhaustiveness checking via NoReturn type (#614)
Adds docs about exhaustive literal and enum checks (#10860)
erictraut commentedon Dec 31, 2021
This appears to work in the latest version of mypy, so I think this could be closed.
bbatliner commentedon Feb 17, 2022
Is the lack of
IntEnumnarrowing/exhaustiveness in mypy well-defined behavior? I ask because Pyright/VS Code are able to narrow thesignal.HandlersIntEnum, for example, based on integer equality comparisons:bbatliner commentedon Feb 17, 2022
Interestingly I can compare the integers with
is(which I think would run because a small range of cpython integers are backed by a single location in memory per integer IIRC?) and get mypy to pass, but generally this should be unsafe at runtime:JelleZijlstra commentedon Feb 17, 2022
Comparing enum values with
isis safe if the value is actually an enum.bbatliner commentedon Feb 17, 2022
Cool, thanks Jelle! So this seems working as intended, then.
hauntsaninja commentedon Apr 24, 2023
Everything in this issue works, and in #11521 I changed mypy to make == narrow enums in addition to is