2025, Oct 17 01:00

Exhaustive handling of Python Enum with assert_never in MyPy: instance vs class members, fixes and safe workarounds

Learn how a MyPy bug affects exhaustive handling of Python Enum when using assert_never via instance access. See fixes, safe workarounds, and PR #19422.

Exhaustive handling of Python Enum values is a common pattern for keeping business logic tight and safe. When a type checker understands that all cases are covered, the final branch with assert_never should be unreachable. But sometimes static analysis stumbles in non-obvious places, especially around instance-versus-class attribute access on Enum members.

Reproducing the issue

The following minimal example captures the problem. The logic compares an Enum instance to members accessed via that same instance. A final else uses assert_never for exhaustiveness checking, but MyPy reports it as reachable.

from enum import Enum
from typing import assert_never

class WeekNum(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3

chosen = WeekNum(1)  # simplified version of larger code base
if chosen == chosen.MONDAY:
    print("MONDAY")
elif chosen == chosen.TUESDAY:
    print("TUESDAY")
elif chosen == chosen.WEDNESDAY:
    print("WEDNESDAY")
else:
    assert_never(chosen)
    # MyPy reports this as reachable, while another linter sees "Never"

What’s really happening

The behavior is not about Enum semantics. It’s a MyPy bug that affects how types are narrowed when Enum members are accessed off the instance rather than via the Enum class. In this shape, MyPy fails to conclude that the else is impossible, even though an equivalent form using the Enum class directly is recognized correctly. This is why switching to direct class references immediately makes the same code pass exhaustiveness checks.

Workable alternatives before the fix

You can write comparisons against the Enum class directly. This form is recognized by MyPy as fully exhaustive, and assert_never is treated as unreachable.

from enum import Enum
from typing import assert_never

class WeekNum(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3

picked = WeekNum(1)
if picked == WeekNum.MONDAY:
    print("MONDAY")
elif picked == WeekNum.TUESDAY:
    print("TUESDAY")
elif picked == WeekNum.WEDNESDAY:
    print("WEDNESDAY")
else:
    assert_never(picked)  # MyPy reveals "Never"

However, directly referencing the Enum class can be problematic in IPython with Autoreload, where a reloaded Enum class may not match the runtime instance’s type. A pragmatic approach that avoids such coupling is to derive the Enum type from the instance. This also satisfies MyPy’s exhaustiveness understanding in practice while staying resilient to Autoreload behavior.

from enum import Enum
from typing import assert_never

class WeekNum(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3

current = WeekNum(1)
enum_cls = type(current)
if current == enum_cls.MONDAY:
    print("MONDAY")
elif current == enum_cls.TUESDAY:
    print("TUESDAY")
elif current == enum_cls.WEDNESDAY:
    print("WEDNESDAY")
else:
    assert_never(current)  # MyPy reveals "Never" and works with IPython Autoreload

Upstream status

That’s a mypy bug. I have fixed that in #19422 a few days ago, your original snippet works on mypy master (will likely be released in 1.18.0).

In other words, once the fix is available to you, the initial pattern that compares against members via the instance is expected to type-check correctly without extra changes.

Why this matters

assert_never acts as a guardrail for exhaustive branching on discriminated unions and Enum types. When a type checker flags it as reachable incorrectly, it undermines confidence in the static analysis, leading to unnecessary refactors or fragile workarounds. In iterative workflows with IPython Autoreload, avoiding direct references to reloaded classes can also prevent confusing runtime/type-checking mismatches. Recognizing that this specific pattern was a tooling bug helps keep your code focused on domain logic rather than dancing around false positives.

Conclusion

If you hit this assert_never reachability warning when comparing an Enum instance to its own members, the root cause is the MyPy bug referenced above. As immediate options, either compare against the Enum class directly, or obtain the Enum type from the instance via type(...) to remain friendly with IPython Autoreload. Once the upstream fix lands in a release that includes PR #19422, the original, concise instance-member pattern should work as intended. Keep your exhaustiveness checks in place—they pay off by catching real gaps while keeping the signal-to-noise ratio high in your type checking pipeline.

The article is based on a question from StackOverflow by Rylix and an answer by STerliakov.