You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
False positive: Statement is [unreachable] in loop with iteration-carried flag #19849
mypy reports Statement is unreachable in a loop that uses a “skip next line” flag across iterations. The flagged branch is reachable in the next iteration, but mypy appears to analyze it as if the flag can’t be True.
To Reproduce
from __future__ importannotationsimportre_HDR_BY_CMD=re.compile(r"^\s*#\s*by the following command:\s*$", re.I)
_HDR_CMD=re.compile(r"^\s*#\s*pip-compile\b", re.I)
_ANY_VIA=re.compile(r"^\s*#\s*via\b.*$", re.I)
defnorm(lines: list[str]) ->list[str]:
out: list[str] = []
skip_next=Falseforrawinlines:
ln=raw.rstrip()
ifskip_nextand_HDR_CMD.match(ln):
skip_next=Falsecontinueif_HDR_BY_CMD.match(ln):
skip_next=Truecontinueif_ANY_VIA.match(ln):
out.append("# via")
else:
out.append(ln)
returnout
@sMiNT0S I can't reproduce the error using 1.18.1 and python 3.13, mypy --warn-unreachable responds with
Success: no issues found in 1 source file
are you sure the snippet is representative and you are really using 1.18.1? Doesn't reproduce with master mypy too, neither locally nor in playground (1.18.1 is not available there yet).
I tried checking the function from your repo (L200-224) in isolation, mypy also does not report any of # type: ignore[unreachable] lines as unreachable, so I'm really not sure what problem you're trying to report. Playground. I did not try to clone your whole repository and run mypy on it (and would prefer to not have any LLM code checked out on my PC for several reasons), please provide a reproducible snippet that demonstrates the issue you're encountering.
@zerlok this works as intended. with statements are complicated: namely, they are able to "swallow" exceptions, and we cannot statically determine whether any given context manager does that. For example,
classCM:
def__enter__(self): returnselfdef__exit__(self, *args): returnTruedeffails() ->int:
ifTrue: raiseRuntimeErrorreturn0defshould_fail() ->int:
withCM():
returnfails() # raises, but exception is swallowed by CM# implicit `return None` is reachable hereassertshould_fail() isNone# passes at runtime
OK, I was able to extract a MRE. The snippet is fine as-is, but you're omitting a very important detail: strict_optional = false in the config file. I'd say nobody should be using that nowadays, especially globally - do you really want it disabled?
cc @tyralla - you worked on reachability analysis recently, right? This is not a recent regression (passes on 1.0.0-1.7.1, fails on 1.8.0-master, I did not bisect further), and I don't know whether this problem is of any importance, but maybe it's a simple fix?)
Thanks for the careful read and the MRE. You’re right: my repo config has strict_optional = false. That’s intentional for now due to mixed-typed edges and third-party bits, but I agree it’s legacy and I’ll migrate toward the default (strict_optional = true) in a follow-up.
To be precise about the reproducer: with strict_optional = false the “Statement is unreachable” shows up; with strict_optional = true it doesn’t (1.18.1). That matches your observation that this started after 1.7.x.
Re the disable_error_code list: it’s a temporary, repo-level compromise to keep CI noise down while I ratchet types. Core packages are checked more strictly via overrides (e.g., disallow_untyped_defs = true, no_implicit_optional = true), and in any case the current report is [unreachable], which isn’t disabled. I’ll narrow the disables and scope them better as I clean up.
I’ve added the missing config detail to the issue body. If this is considered WAI under no-strict-optional, feel free to mark it as such; otherwise the MRE + config should make it easy to poke at the reachability bit.
EDIT:
Side note, couldn't let "I did not try to clone your whole repository and run mypy on it (and would prefer to not have any LLM code checked out on my PC for several reasons)" go.. :
this repo runs any model-generated code in a sandboxed child process with guard injection (sitecustomize), filesystem confinement to a temp root, subprocess and network disabled by default, and per-run resource limits. CI refuses to run if the security audit fails. Details live in SAFETY.md and SECURITY.md. Safety is a first-class feature here, not an afterthought. Core controls include:
• guarded import layer that denies risky modules/APIs
• temp-root filesystem jail with explicit allow-lists for read-only fixtures
• network egress and subprocess creation blocked by default; opt-in only via CLI flags
• per-run CPU/memory caps and wall-clock timeouts; clean teardown between runs
• honest CLI banner reflecting actual enforcement
🔒 AIBugBench Security Audit (excerpt)
✅ Sandbox Implementation: PASS — core primitives present
✅ Validator Integration: PASS — sandbox decorator active
✅ CLI Security Flags: PASS — flags + banner detected
✅ Temp workdir: PASS — sandbox uses temp directory
✅ Resource limits wired: PASS — CPU/memory limits present
✅ Network block: PASS — socket creation guarded
✅ Subprocess block: PASS — subprocess execution blocked
✅ Dynamic code block: PASS — eval/exec/compile blocked
✅ Dangerous imports block: PASS — dangerous imports blocked
✅ Windows Job Objects: PASS — Job Objects support implemented
✅ Banner honesty: PASS — banner consistent with implementation
⏸ Env whitelist: DEFERRED — environment partially scrubbed (pattern removal + clear on exit)
⏸ Python isolation: DEFERRED — -I/-S not used (guards load via sitecustomize)
✅ Filesystem bounds: PASS — comprehensive filesystem guard active
✅ GitHub security config: PASS — security workflows present
✅ PR Security Automation: PASS — workflow active
✅ Canary CPU loop: PASS — timed out as expected
❌ Canary Network: FAIL — import error: No module named 'requests'
⏸ Canary Subprocess: DEFERRED — secure_runner import failed: No module named 'requests'
⏸ Canary Process Spawn: DEFERRED — secure_runner import failed: No module named 'requests'
⏸ Canary Dynamic Code: DEFERRED — secure_runner import failed: No module named 'requests'
⏸ Canary Dangerous Imports: DEFERRED — secure_runner import failed: No module named 'requests'
⏸ Canary Filesystem: DEFERRED — secure_runner import failed: No module named 'requests'
Summary: Mandatory passed: 12/17 (DEFERRED: 4). Optional/informational: 6.
Result: Audit failed closed on the mandatory network canary (by design, the runner aborts on any mandatory miss). You can opt-in to network or run in --unsafe if you need to exercise that path explicitly.
I think it's OK to keep this open now, the [unreachable] reported is still a false positive, no matter what configuration flags are used.
it’s a temporary, repo-level compromise
Ah, well, that's definitely fine then - applying typing gradually definitely makes sense!
this repo runs any model-generated code...
Not gonna discuss it here, but in brief - I just do not want to interact with any attempts at LLM-based code generation, it doesn't matter how secure the system is, I simply refuse to treat tokens emitted by LLMs as code and let them float around my dev environments. That's my own position, not endorsed by mypy devs or any other group I might be associated with, I just want to stay away from LLM-generated texts/code/images/dragons, as far as possible. I had to work with LLMs for plain text (not code - just text summaries) generation last year, and this was a truly horrible experience I hope to never return to again. I tried to integrate Claude and Cursor into my workflows, only to abort these attempts after 2 or 3 weeks of suffering through. Please, let me just avoid touching LLMs, especially in any professional contexts:)
Understood, keeping this open makes sense since it’s a false positive regardless of flags.
On LLMs: totally understandable. I do work with them, but the main reason I started my project/repo was exactly what you’re describing. Not to dump a life story, but after a months long hyperfocus on Python/dev environments, noticing how responses changed around specific snippets pushed me to build a benchmark. It can be extremely frustrating, so I completely get your position.
with statements are complicated: namely, they are able to "swallow" exceptions, and we cannot statically determine whether any given context manager does that. For example,
You right, I forgot about that.
Taking into account that CM can swallow the exception and the interpreter will just exit the scope and continue the execution, doesn't that mean that mypy should highlight possible undefined vars (spoiler it didn't in my case)?
asyncdefrun_machine(
sm: AnyioStreamStateMachine[StreamMessage, str],
streams: MemoryStreams,
inputs: t.Sequence[StreamMessage],
) ->t.Sequence[str]:
asyncwithcreate_task_group() astg:
tg.start_soon(send_inputs, streams, inputs)
tg.start_soon(sm.run, streams.input_receive, streams.output_send)
outputs=awaitcollect_outputs(streams)
returnoutputs# should error: Name "outputs" may be undefined [possibly-undefined]
Thus I have to write the code like this:
asyncdefrun_machine(
sm: AnyioStreamStateMachine[StreamMessage, str],
streams: MemoryStreams,
inputs: t.Sequence[StreamMessage],
) ->t.Sequence[str]:
output=list[str]() # define the var for output value from before the CM scopeasyncwithcreate_task_group() astg:
tg.start_soon(send_inputs, streams, inputs)
tg.start_soon(sm.run, streams.input_receive, streams.output_send)
outputs=awaitcollect_outputs(streams)
returnoutputs
P.S. I think it would be nice if mypy can distinguish CM that can swallow the error and the CM that won't do that. Not sure if current builtin interfaces of ContextManager / AsyncContextManager allow to do that, because all exit methods return bool | None . Maybe some new SimpleContextManager with exit -> None should be introduced.
cc @tyralla - you worked on reachability analysis recently, right? This is not a recent regression (passes on 1.0.0-1.7.1, fails on 1.8.0-master, I did not bisect further), and I don't know whether this problem is of any importance, but maybe it's a simple fix?)
I recently worked on a few reachability issues where loops and similar things were involved, but this one seems to be different:
importrex: re.Match[str] |Noneifnotx:
x=None# error: Statement is unreachable [unreachable]
Does anyone know what makes Match so special here?
I am a little busy at the moment, but I could investigate later.
There are no plans to remove the evil no-strict-optional option in Mypy 2, right?
Activity
zerlok commentedon Sep 15, 2025
+1
Mypy: 1.18.1
or
Outputs:
The only possible solution:
https://github.com/zerlok/statomata/actions/runs/17654820898/job/50174804171?pr=11
sterliakov commentedon Sep 15, 2025
@sMiNT0S I can't reproduce the error using 1.18.1 and python 3.13,
mypy --warn-unreachableresponds withare you sure the snippet is representative and you are really using 1.18.1? Doesn't reproduce with
mastermypy too, neither locally nor in playground (1.18.1 is not available there yet).I tried checking the function from your repo (L200-224) in isolation, mypy also does not report any of
# type: ignore[unreachable]lines as unreachable, so I'm really not sure what problem you're trying to report. Playground. I did not try to clone your whole repository and runmypyon it (and would prefer to not have any LLM code checked out on my PC for several reasons), please provide a reproducible snippet that demonstrates the issue you're encountering.@zerlok this works as intended.
withstatements are complicated: namely, they are able to "swallow" exceptions, and we cannot statically determine whether any given context manager does that. For example,sterliakov commentedon Sep 15, 2025
OK, I was able to extract a MRE. The snippet is fine as-is, but you're omitting a very important detail:
strict_optional = falsein the config file. I'd say nobody should be using that nowadays, especially globally - do you really want it disabled?cc @tyralla - you worked on reachability analysis recently, right? This is not a recent regression (passes on 1.0.0-1.7.1, fails on 1.8.0-master, I did not bisect further), and I don't know whether this problem is of any importance, but maybe it's a simple fix?)
Here's a playground link with the repro.
BTW, this:
is essentially equivalent to "do not type check at all", you disabled almost all codes that indicate real type errors...
sMiNT0S commentedon Sep 15, 2025
@sterliakov
Thanks for the careful read and the MRE. You’re right: my repo config has strict_optional = false. That’s intentional for now due to mixed-typed edges and third-party bits, but I agree it’s legacy and I’ll migrate toward the default (strict_optional = true) in a follow-up.
To be precise about the reproducer: with strict_optional = false the “Statement is unreachable” shows up; with strict_optional = true it doesn’t (1.18.1). That matches your observation that this started after 1.7.x.
Re the disable_error_code list: it’s a temporary, repo-level compromise to keep CI noise down while I ratchet types. Core packages are checked more strictly via overrides (e.g., disallow_untyped_defs = true, no_implicit_optional = true), and in any case the current report is [unreachable], which isn’t disabled. I’ll narrow the disables and scope them better as I clean up.
I’ve added the missing config detail to the issue body. If this is considered WAI under no-strict-optional, feel free to mark it as such; otherwise the MRE + config should make it easy to poke at the reachability bit.
EDIT:
Side note, couldn't let "I did not try to clone your whole repository and run mypy on it (and would prefer to not have any LLM code checked out on my PC for several reasons)" go.. :
this repo runs any model-generated code in a sandboxed child process with guard injection (sitecustomize), filesystem confinement to a temp root, subprocess and network disabled by default, and per-run resource limits. CI refuses to run if the security audit fails. Details live in SAFETY.md and SECURITY.md. Safety is a first-class feature here, not an afterthought. Core controls include:
• guarded import layer that denies risky modules/APIs
• temp-root filesystem jail with explicit allow-lists for read-only fixtures
• network egress and subprocess creation blocked by default; opt-in only via CLI flags
• per-run CPU/memory caps and wall-clock timeouts; clean teardown between runs
• honest CLI banner reflecting actual enforcement
sterliakov commentedon Sep 16, 2025
I think it's OK to keep this open now, the
[unreachable]reported is still a false positive, no matter what configuration flags are used.Ah, well, that's definitely fine then - applying typing gradually definitely makes sense!
Not gonna discuss it here, but in brief - I just do not want to interact with any attempts at LLM-based code generation, it doesn't matter how secure the system is, I simply refuse to treat tokens emitted by LLMs as code and let them float around my dev environments. That's my own position, not endorsed by mypy devs or any other group I might be associated with, I just want to stay away from LLM-generated texts/code/images/dragons, as far as possible. I had to work with LLMs for plain text (not code - just text summaries) generation last year, and this was a truly horrible experience I hope to never return to again. I tried to integrate Claude and Cursor into my workflows, only to abort these attempts after 2 or 3 weeks of suffering through. Please, let me just avoid touching LLMs, especially in any professional contexts:)
sMiNT0S commentedon Sep 17, 2025
Understood, keeping this open makes sense since it’s a false positive regardless of flags.
On LLMs: totally understandable. I do work with them, but the main reason I started my project/repo was exactly what you’re describing. Not to dump a life story, but after a months long hyperfocus on Python/dev environments, noticing how responses changed around specific snippets pushed me to build a benchmark. It can be extremely frustrating, so I completely get your position.
zerlok commentedon Sep 20, 2025
@sterliakov
You right, I forgot about that.
Taking into account that CM can swallow the exception and the interpreter will just exit the scope and continue the execution, doesn't that mean that mypy should highlight possible undefined vars (spoiler it didn't in my case)?
Thus I have to write the code like this:
P.S. I think it would be nice if mypy can distinguish CM that can swallow the error and the CM that won't do that. Not sure if current builtin interfaces of ContextManager / AsyncContextManager allow to do that, because all exit methods return
bool | None. Maybe some newSimpleContextManagerwithexit -> Noneshould be introduced.tyralla commentedon Sep 21, 2025
I recently worked on a few reachability issues where loops and similar things were involved, but this one seems to be different:
Does anyone know what makes
Matchso special here?I am a little busy at the moment, but I could investigate later.
There are no plans to remove the evil
no-strict-optionaloption in Mypy 2, right?tyralla commentedon Oct 10, 2025
The only "special" thing about
Matchcausing the problem seems to be that it is decorated withfinal:prevent false unreachable warnings for @Final instances that occur wh…