Asking a question. I'd like to know if there is a standard idiom for enforcing exhaustive matching of an Enum / Literal type.
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 @bluetech here. It looks like this works for Unions, but not for Enums / Literals? Apologies if this has been covered before.
def assert_never(x: Any) -> NoReturn:
assert False, "Unhandled type: {}".format(type(x).__name__)
PetLiteral = Literal['dog', 'cat', 'iguana']
def make_sound_literal(pet: PetLiteral) -> str:
if pet == 'dog':
return 'woof'
elif pet == 'cat':
return 'meow'
# elif pet == 'iguana':
# return 'confused silence'
else:
assert_never(pet)
If I comment out the iguana branch and check this file with:
$ mypy --strict pet_sounds.py
mypy has no complaints. If I try the same deal with an enum:
class PetEnum(Enum):
dog: str = 'dog'
cat: str = 'cat'
iguana: str = 'iguana'
def make_sound_enum(pet: PetEnum) -> str:
if pet == PetEnum.dog:
return 'woof'
elif pet == PetEnum.cat:
return 'meow'
# elif pet == PetEnum.iguana:
# return 'confused silence'
else:
assert_never(pet)
and check again with --strict, I still get no complaints.
What are the versions of mypy and Python you are using?
mypy 0.660
Python 3.6.5
Do you see the same issue after installing mypy from Git master?
Yes, got the same behavior with mypy 0.680+dev.21c8a812c697baf7394eafe360188ededcec6d9c
--strictThe assert_never() signature proposed in the issue you reference is a bit different:
def assert_never(x: NoReturn) -> NoReturn:
assert False, "Unhandled type: {}".format(type(x).__name__)
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 https://github.com/python/mypy/issues/5935. I am not sure about the timeline however, @Michael0x2a will you have time to work on this?
Note that https://github.com/python/mypy/issues/4223 gives an example where this can cause a false positive Missing return statement.
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.
And on mypy 0.780 it seems Literal is working too:
def _assert_never(x: NoReturn) -> NoReturn:
assert False, "Unhandled type: {}".format(type(x).__name__)
def f(x: Literal['a', 'b', 'c']) -> None:
if x == 'a':
pass
elif x == 'b':
pass
else:
_assert_never(x)
correctly errors:
error: Argument 1 to "_assert_never" has incompatible type "Literal['c']"; expected "NoReturn"
Found 1 error in 1 file (checked 1 source file)
Note that you have to use is to test enum equality and not ==
Most helpful comment
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.