Skip to content

nested class pattern inside generic wrapper is not considered exhaustive #3731

Description

@lennardwalter

Describe the Bug

You can assign me to this, I will PR.

Pyrefly does not consider a nested class pattern inside a generic wrapper class exhaustive.
A common result-like pattern:

match result:
    case Ok(value):
        ...
    case Err(SomeError()):
        ...

still leaves Pyrefly thinking the Err[SomeError] case can fall through.

Repro

from typing import assert_never
class Ok[T]:
    __match_args__ = ("value",)
    value: T
class Err[E]:
    __match_args__ = ("value",)
    value: E
class NotFound:
    pass
def f(r: Ok[int] | Err[NotFound]) -> int:
    match r:
        case Ok(value):
            return value
        case Err(NotFound()):
            raise Exception()
def g(r: Ok[int] | Err[NotFound]) -> int:
    match r:
        case Ok(value):
            return value
        case Err(NotFound()):
            raise Exception()
        case _:
            assert_never(r)

Actual Behavior

Pyrefly reports bad-return for f:

ERROR Function declared to return `int`, but one or more paths are missing an explicit `return` [bad-return]

For g, Pyrefly reports that the fallback is still reachable:

ERROR Argument `Err[NotFound]` is not assignable to parameter `arg` with type `Never`

Expected Behavior

Both functions should pass.
Since r has type:

Ok[int] | Err[NotFound]

and the match handles:

case Ok(value)
case Err(NotFound())

there should be no remaining possible value for the fallback.

Keyword Pattern Variant Also Fails

The equivalent keyword pattern also fails:

def h(r: Ok[int] | Err[NotFound]) -> int:
    match r:
        case Ok(value=value):
            return value
        case Err(value=NotFound()):
            raise Exception()
        case _:
            assert_never(r)

This seems related to match exhaustiveness, but it is not fixed by the issue #1286 fix. That fix handles direct union subjects like A | B; this case involves eliminating an outer generic wrapper arm based on an exhaustive nested pattern on its matched attribute.
Possibly relevant implementation details:

  • Positional class patterns appear to have a TODO around narrowing attributes from __match_args__.
  • Keyword class patterns already create an attribute/facet subject, but the narrowed facet becoming impossible does not seem to eliminate the containing wrapper type.

Sandbox Link

https://pyrefly.org/sandbox/?project=N4IgZglgNgpgziAXKOBDAdgEwEYHsAeAdAA4CeS4ATrgLYAEALqcROgOZ0Q3G6UN2o4cGHwD66GADcRAHXQBjKILh0A8gGsA2gBUAuojl0jdUaJqoG8gBajUlNnFN0AvHQAUMkJNRQArjE8AGgBKQ2NvPxhEOm05RWU6AFFKSk1E-TCjU3NLGzsHJ1cPLx9-IND0YzoI-2jEuKUhOgA5XAYAMVxfLANK42JlOUwYMDowN0pojU1WBl06AB8klM1Wjq6sXWC6AFoAPk50Bl6qnOs6ScyqunlBGDV1NxqYYJPr98oYBl9KSuerqq3YTLShuNadbqYNzBV4A94XVAQYGJfDyGDEBgQXDoaFDEZ0NgTKZaWbzJbJVLgjaYLa7A6zN5GM5WC6M65A%2B4aJ6lF5s%2BEXL4-P48uFGDkgsFtCFYaGwvr8oyURHI1HozHY3Hy9l3Ex8%2BHKEQMcRSEQTYIgQIgMifMBQUiEBi0KAUADEdAACqQbXa6GgsHh8DdsZA2D8LFj0IQ5G6AMowe5WBgMYhwRAAejT1pGdsIvDYaZg6DTmFw8jgafkwYgoaV6qLY14Am80FQ2FgQfQIbDdbouAxEbgUfQZAYVmxO2klDgEZcdE8AGZCABGABMnjkmhE1Cnujk3S4PD4MEwO0wEE%2B8kx0lnAHJq%2BheDAb3IHwwdp8AI6%2Bc-HnbqGCkDsqDyGiTSuDeADudjoM%2B6AgAAvpawFXjA7TQDAFB%2BjgBAkOQ8FAA

(Only applicable for extension issues) IDE Information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions