Mypy doesn't narrow a matched object's type (generic or not) based on attribute subpattern match #19081
Labels
bug
mypy got something wrong
topic-match-statement
Python 3.10's match statement
topic-type-narrowing
Conditional type narrowing / binder
Bug Report
Summary
TLDR
Mypy doesn't narrow a parent object's type when subpattern-matching on one of its attributes.
Longer summary
Mypy doesn't always use attribute sub-pattern matches (e.g.
case Err(ValueError())
) to narrow the parent object (e.g.Err[ValueError | AttributeError]
isn't narrowed toErr[ValueError]
). This prevents an error-free type-level exhaustiveness check usingassert_never
after matching all possible patterns.Some of the examples below show that mypy successfully narrows the attribute itself, but doesn't propagate this narrowing up to the object being matched, even when there's a generic type argument that could be narrowed. E.g. if
val
is narrowed toValueError
, mypy should be able to narrow the object fromErr[ValueError | AttributeError]
toErr[ValueError]
.Ideally, in the
case _
after exhausting all patterns/subpatterns, the object could be narrowed toNever
.It's possible that it's expected that mypy can't narrow a non-generic type, if it needs a type argument that can explicitly be narrowed. I've included a non-generic example below anyway though, for completeness, since it does have all its patterns matched but fails the exhaustiveness check.
Context
The real-world context for this issue was an attempt to pattern match on poltergeist's
Result = Ok[_T] | Err[_E]
type, where Err can be constrained to a specific subset of exceptions. Finishing amatch result:
statement withcase _: assert_never(result)
only works if we avoid matching error sub-patterns: i.e. if we docase Err(err)
and avoidcase Err(ValueError())
.In this context, this issue takes away from some of the potential power of a library like poltergeist, which seeks to make error handling more explicit and type-safe.
I guess a workaround could be to just add a nested
match
statement onerr._value
itself within thecase Err(err)
block. But that feels unfortunate to have to do whenmatch
was built to be powerful around subpattern matching, and PEP 636 – Structural Pattern Matching: Tutorial states that "Patterns can be nested within each other" (which is the case here, it's just the type-checking that doesn't use all the type info it has).To Reproduce
Example 1: Result (generic)
Here, I'd expect mypy to narrow the type of
result
, e.g. toErr[ValueError]
inside thecase Err(ValueError() as val)
block, and toNever
inside thecase _
block.Example 2: NonGenericErr (non-generic)
Here, I narrowed the scope to matching on the error class, and made it non-generic. Even here, mypy can narrow the attribute (
val: ValueError
), but not the object (err: NonGenericErr
anderr._value: ValueError | AttributeError
). And doesn't realize in thecase _
block that we've already exhausted all patterns above.Example 3: FailureResult (generic, dataclass)
I could see the logic that Example 2 is constrained by the lack of a generic type for mypy to use to narrow
err
beyondNonGenericErr
. But even if we add that back, mypy can't narrowerr
as expected within any of thecase
blocks.This example is basically a trimmed down / tighter-scoped version of Example 1.
Expected Behavior
Mypy successfully narrows the type of an object we're pattern matching on, based on how we've matched on its attribute, allowing for an exhaustive
match
statement ending withassert_never
if we have indeed exhausted all possible patterns.Actual Behavior
See errors and unexpected
reveal_type
outputs aboveYour Environment
mypy.ini
(and other config files):`mypy.ini`
Related issues
Most of these linked issues are specific to tuples, though. But since
__match_args__
is a tuple, and/or since a tuple could be seen as a generic typeTuple[...]
, I could see the root issues here overlapping.Thanks for all the work done to make mypy what it is!
The text was updated successfully, but these errors were encountered: