Skip to content

match-case does not work correctly with Literals vs Enums #20142

Open

Description

@randolf-scholz
from enum import StrEnum
from typing import Literal, assert_never, reveal_type

class Color(StrEnum):
    RED = "r"
    GREEN = "g"
    BLUE = "b"

def test_literal_as_enum(x: Literal["g"] = "g") -> None:
    match x:
        case Color.RED:
            assert_never(x)
        case Color.GREEN:
            reveal_type(x)
        case Color.BLUE:
            assert_never(x)
        case _:
            assert_never(x)

def test_enum_as_litearl(y: Literal[Color.BLUE] = Color.BLUE) -> None:
    match y:
        case "r":
            assert_never(y)
        case "g":
            assert_never(y)
        case "b":
            reveal_type(y)
        case _:
            assert_never(y)

test_literal_as_enum()  # selects green branch at runtim
test_enum_as_litearl()  # selects blue branch at runtime

Expected no errors, but mypy-playground reports

main.py:14: error: Statement is unreachable  [unreachable]
main.py:18: error: Argument 1 to "assert_never" has incompatible type "Literal['g']"; expected "Never"  [arg-type]
main.py:27: error: Statement is unreachable  [unreachable]
main.py:29: error: Argument 1 to "assert_never" has incompatible type "Literal[Color.BLUE]"; expected "Never"  [arg-type]

Related: https://discuss.python.org/t/amend-pep-586-to-make-enum-values-subtypes-of-literal/59456
Also: #16327, #17162, #19616 (but none of these is specifically about match-case)

Activity

  1. sterliakov commented on Oct 30, 2025

    @sterliakov
    Collaborator

    #19594 should (at least partially) address this issue: it will still report fallback assert_never as failing (and I'm not sure whether that's correct), but reveal_type will be reachable and reveal Literal[Color.Green] and Literal["b"], resp.

  2. randolf-scholz commented on Oct 30, 2025

    @randolf-scholz
    ContributorAuthor

    but reveal_type will be reachable and reveal Literal[Color.Green] and Literal["b"], resp.

    Uhm, that is exactly the wrong way around. The literal and value patterns are equality based, so in the first case it should be Literal["g"] and in the latter it should be Literal[Color.BLUE]. Alex actually added some nice extra detail in his issue report in ty after I brought this problem up in discourse.

  3. sterliakov commented on Oct 30, 2025

    @sterliakov
    Collaborator

    Ough, and you're right, otherwise

    class Foo(StrEnum):
        A = 'a'
        
        def meth(self) -> None: ...
    
    
    a: Literal['a'] = 'a'
    match a:
        case Foo.A:
            a.meth()

    will pass. Thanks!

    The literal and value patterns are equality based

    This is a nice observation. I wonder why visit_value_pattern uses conditional_types_with_intersection instead of the equality helper then...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions