Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

type narrowing vs assert vs unreachability changed in PR #17818 (between mypy 1.11 and 1.12) #18431

Open
benclifford opened this issue Jan 8, 2025 · 0 comments
Labels
bug mypy got something wrong

Comments

@benclifford
Copy link

benclifford commented Jan 8, 2025

Bug Report

tldr, since PR #17817 release in mypy 1.12, and still this way in master 11cc21c, I have seen this behaviour change in the interaction of type narrowing, assert and unreachability checking:

# prereq  foo.x: int = 0
assert not foo.x           # narrows foo.x to Literal[0]
foo.reset()                # modifies `foo.x=4`, so `foo.x: Literal[0] = 4` (!)
assert foo.x               # at runtime this passes, but mypy reasons Literal[0] cannot pass this assert
# BAD: anything after here is warned as unreachable

mypy believes assert not foo.x and assert foo.x cannot both pass, mediated via type narrowing, across an opportunity for an object to change its own state.

To Reproduce

$ cat mp818.py 
from typing import Union
 
class Foo:
  def __init__(self):
    self.x: int = 0
 
  def reset(self):
    self.x=4
 
foo=Foo()
 
reveal_type(foo.x)
assert not foo.x
reveal_type(foo.x)
 
# SWAP THESE LINES TO MAKE MYPY HAPPY
# foo.x = 4
foo.reset()
 
reveal_type(foo.x)
assert foo.x
 
print(foo.x)
 
 
$ mypy mp818.py --warn-unreachable
 
# BAD, >= mypy PR #17818
 
 
mp818.py:5: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
mp818.py:12: note: Revealed type is "builtins.int"
mp818.py:14: note: Revealed type is "Literal[0]"
mp818.py:20: note: Revealed type is "Literal[0]"
mp818.py:23: error: Statement is unreachable  [unreachable]
Found 1 error in 1 file (checked 1 source file)

# GOOD, < PR #17818 *or* swap foo.reset() to foo.x=4
 
 
mp818.py:5: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
mp818.py:12: note: Revealed type is "builtins.int"
mp818.py:14: note: Revealed type is "builtins.int"
mp818.py:20: note: Revealed type is "builtins.int"
Success: no issues found in 1 source file

I'm encountering this in real life at https://github.com/Parsl/parsl/blob/34a2890c65f239bb145dca6af76bbdcc0443bc3e/parsl/tests/test_curvezmq.py#L310 where we assert that a socket is open, do something that should close it, then assert that it is really is closed.

Expected Behavior

I expected the behaviour to be the pre-17818 behaviour: if i assert on a field inside an object, don't carry reasoning from earlier asserts about that value across calls that might modify that value.

Actual Behavior

An assertion about the state of an object is carried across a mutation of that state and used to make incorrect reasoning about later asserts.

Your Environment

works with: mypy 1.11, 1.5.1
fails with: mypy 1.12, f6520c8, master yesterday ccf05db

  • Mypy command-line flags:

mypy mp818.py --warn-unreachable (mp818.py is my reproducer above)

  • Mypy configuration options from mypy.ini (and other config files):

none

  • Python version used:

Python 3.12

@benclifford benclifford added the bug mypy got something wrong label Jan 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

1 participant