Skip to content

Detect exhaustiveness in partially-defined variable checks #13926

Open

Description

@ilinum

For the partially defined check (--enable-error-code partially-defined), mypy generates a false-positive in the following case:

def f1(x: int | str) -> int:
  if isinstance(x, int):
    y = 1
  elif isinstance(x, str): 
    y = 2 
  return y # error: Name "y" may be undefined

The same applies to match statements:

def f(x: int | str) -> int:
  match x: 
    case int():
      y = 1 
    case str():
      y = 2 
  return y # error: Name "y" may be undefined

It's likely that mypy already detects this somewhere, since it doesn't complain about the missing return:

def f3(x: int | str) -> int:
  if isinstance(x, int):
    return 1
  elif isinstance(x, str): 
    return 2

Activity

  1. ilinum commented on Nov 22, 2022

    @ilinum
    CollaboratorAuthor

    @JukkaL (or anyone else), I would appreciate some help here because I'm a bit stuck.

    I have discovered has two places where it finds unreachable code:

    • Semantic Analyzer
    • Conditional Type Binder

    Semantic Analyzer marks blocks with is_unreachable = True, so it's pretty easy to use (implemented in #14161). However, the exhaustiveness of type checks logic is located Conditional Type Binder, which is where it handles cases as described in the ticket.

    Logic using conditional type binder:

    self.binder.is_unreachable()

    What I would like to do is to store information about unreachable blocks somewhere. My original approach, was to set block.is_unreachable = True in checker.py. This works with cases like:

    def f1(x: int | str) -> int:
      if isinstance(x, int):
        y = 1
      elif isinstance(x, str): 
        y = 2 
      else:
        pass
      return y # No error any more.

    However, this only works if there is a body but not if the body is missing (so the original examples here don't work). We can add special fields to the IfStmt and MatchStmt classes that indicate reachability of different branches but that sounds a bit iffy to me. Not that I have better ideas :)

    Can you think of a good way of storing branch reachability information somewhere?

  2. JukkaL commented on Nov 22, 2022

    @JukkaL
    Collaborator

    Can you think of a good way of storing branch reachability information somewhere?

    Nothing comes to mind immediately, but I'll think about this more tomorrow.

  3. ilinum commented on Feb 24, 2023

    @ilinum
    CollaboratorAuthor

    @JukkaL would appreciate some pointers here if you have a bit of time! :)

  4. JukkaL commented on Feb 24, 2023

    @JukkaL
    Collaborator

    Sorry, lost track of this. I'll think about this next week (keep pinging me if you don't hear anything by mid next week).

  5. JukkaL commented on Feb 27, 2023

    @JukkaL
    Collaborator

    @ilinum I think reusing block.is_unreachable sounds like the best approach. If there is no block, how about just replacing the None value for block with an empty block (Block([]), but only if the flag needs to be set so that this won't increase memory use significantly?

    For example, if else_body is None and it's unreachable, we'd assign a Block([]) to else_body during type checking and set is_unreachable = True for this block. If it's reachable (the vast majority of cases), we'd keep the None value, i.e. a None value would implicitly mean "empty but reachable".

  6. erictraut commented on Aug 13, 2023

    @erictraut

    Was the "partially-defined" error code ever implemented? I don't see any reference to it in the mypy docs or sources. I'm not able to repro the error with the code sample, but perhaps I'm not configuring mypy correctly. Or maybe this issue has been fixed.

  7. AlexWaygood commented on Aug 25, 2023

    @AlexWaygood
    Member

    Was the "partially-defined" error code ever implemented?

    yes, but the name was changed to possibly-undefined. You need to use --enable-error-code=possibly-undefined to enable it.

  8. cyphase commented on Mar 8, 2026

    @cyphase

    The if/elif example is fixed in #20149 (269e7ee), but the match example is still false-positive as of the latest commit on master:

    $ uvx --from "git+https://github.com/python/mypy.git@6fcb8f08ca08bd9c1884c44783d7d93a6584dfe0" mypy -c '
    def f1(x: int | str) -> int:
        if isinstance(x, int):
            y = 1
        elif isinstance(x, str):
            y = 2
        return y
    def f(x: int | str) -> int:
        match x:
            case int():
                y = 1
            case str():
                y = 2
        return y
    ' --enable-error-code=possibly-undefined
    
    <string>:15: error: Name "y" may be undefined  [possibly-undefined]
    Found 1 error in 1 file (checked 1 source file)

    Same with an enum:

    $ uvx --from "git+https://github.com/python/mypy.git@6fcb8f08ca08bd9c1884c44783d7d93a6584dfe0" mypy -c '
    import enum
    class Color(enum.Enum):
        RED = "red"
        BLUE = "blue"
    def with_if(c: Color) -> str:
        if c is Color.RED:
            passes = "red"
        elif c is Color.BLUE:
            passes = "blue"
        else:
            from typing import assert_never
            assert_never(c)
        return passes
    def with_match(c: Color) -> str:
        match c:
            case Color.RED:
                fails = "red"
            case Color.BLUE:
                fails = "blue"
        return fails
    ' --enable-error-code=exhaustive-match --enable-error-code=possibly-undefined
    
    <string>:26: error: Name "fails" may be undefined  [possibly-undefined]
    Found 1 error in 1 file (checked 1 source file)
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