microsoft / pyright

Static Type Checker for Python
Other
13.28k stars 1.44k forks source link

reportMatchNotExhaustive false positive matching type with non-instantiable base #9291

Closed joshMaybell closed 3 hours ago

joshMaybell commented 3 hours ago

Describe the bug

When using a match statement to test the type of an object, pyright expects cases even for non-instantiable abstract base classes.

Code or Screenshots

import abc

class Base(abc.ABC):
    @abc.abstractmethod
    def _(self): ...

class ChildA(Base):
    def _(self): ...

class ChildB(Base):
    def _(self): ...

def test(obj: Base):
    match obj:
        case ChildA():
            pass
        case ChildB():
            pass

The above snippet causes a reportMatchNotExhaustive flag to be raised in the test function. Modifying the match block to include a branch for Base resolves the error:

def test(obj: Base):
    match obj:
        case ChildA():
            pass
        case ChildB():
            pass
        case Base():
            pass

Of course, attempting to call test with an instance of type Base is not possible because such an object cannot be created.

This issue also exists when using typing.Protocol instead of abc.ABC. However, in this case the inclusion of the case Base() branch results in an additional reportGeneralTypeIssues flag because Base is not @runtime_checkable.

VS Code extension or command-line The above snippets result in the described behavior in at least the following environments (tested only in strict mode):

python 3.11.10: coc-pyright version 1.1.380 pyright (vscode) version 1.1.385 pylance (vscode) version 2024.10.1 pyright (cli) version 1.1.380

Python 3.12.3: pyright (cli) version 1.1.385

erictraut commented 3 hours ago

Pyright's behavior here is correct. There are potentially an infinite number of classes that derive from Base, so exhaustion cannot be statically verified.

Your options are:

  1. Disable match exhaustion by disabling the reportMatchNotExhaustive check in your project
  2. Suppress this specific diagnostic using a # type: ignore or # pragma: ignore comment
  3. Add a wildcard case pattern
  4. Define a type that is a union of all "ChildX" classes that test accepts

type ChildTypes = ChildA | ChildB

def test(obj: ChildTypes):
    ...

My recommendation is option 4. I use this technique frequently in my code.

joshMaybell commented 3 hours ago

Makes sense. Thanks for the suggestions!