Mypy: Exhaustive Matching on Enums / Literals

Created on 9 Feb 2019  路  5Comments  路  Source: python/mypy

  • 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 @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

  • What are the mypy flags you are using? (For example --strict-optional)
    --strict
false-positive feature priority-1-normal topic-literal-types

Most helpful comment

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.

All 5 comments

The 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 ==

Was this page helpful?
0 / 5 - 0 ratings