1.16 regression: `--warn-unused-ignore` now complains in ignored-but-correct places
this appears to have changed behaviour in mypy 1.15 -- I personally prefer the behaviour in 1.15 but would understand if it needed to change. I don't see it mentioned in the changelog however
Bug Report
we're working on incrementally typing a large codebase -- often this means fixing a particular callsite or file at a time (sometimes by intentionally ignoring errors unfortunately)
it seems that in a module with a particular error code (temporarily) disabled, --warn-unused-ignores will complain about a "correct" type ignore (I say "correct" here as it will complain when the file is no longer blocklisted) -- will provide a small example below
To Reproduce
# t.py
def f() -> None:
x: int = 'hi' # type: ignore[assignment]
[mypy]
warn_unused_ignores = true
[mypy-t]
disable_error_code = assignment
Expected Behavior
$ mypy --version
mypy 1.15.0 (compiled: yes)
$ mypy t.py
Success: no issues found in 1 source file
Actual Behavior
$ mypy --version
mypy 1.16.0 (compiled: yes)
$ mypy t.py
t.py:2: error: Unused "type: ignore" comment [unused-ignore]
Found 1 error in 1 file (checked 1 source file)
Your Environment
- Mypy version used: 1.16.0 (regression from 1.15.0)
- Mypy command-line flags: (see above)
- Mypy configuration options from
mypy.ini(and other config files): (see above) - Python version used: 3.13.1
this seems intentional via #11059 and #18849 (bisected to 6b686615dd9fba32af3395d5eeefe2812997c7be)
I'd still argue this is worse than before for my use case and I'm not quite sure what the motivation was in the original issue -- cc @Akuli @brianschubert
actually I do see it in the notes now -- I was searching for the wrong thing (warn-unused-ignore)
Yeah, this was an intentional change. The ignore is unused in the sense it isn't necessary under the current configuration and removing it would not result in a diagnostic being emitted. This is generally a useful behavior, since it lets you detect # type: ignore comments that become no longer relevant after a change in configuration.
As a workaround, you can try disabling the unused-ignore error code on a per-file or line-by-line basis. For example, mypy will accept the following regardless of whether the assignment error code is enabled or not:
def f() -> None:
x: int = 'hi' # type: ignore[assignment,unused-ignore]
I'll note that the previous behavior was buggy in that only some errors could be both ignored and disabled without setting off --warn-unused-ignores. This has to do with how disabled error codes are internally handled in mypy, where in some cases mypy will check whether an error code is enabled before doing some analysis, while in other cases mypy will unconditionally do some analysis and then latter suppress errors if the associated error code is disabled. Previously, checks of the former type would set off --warn-unused-ignores when the error code was disabled while checks of the latter type would not. Now, both behave in the same way.
Also FWIW, the current behavior is consistent with how other static analysis tools I've used work. For example, ruff's RUF100 rule handles unused # noqa comments in much the same way. In ruff, if you silence an error with # noqa and later disable the rule that was associated with that error, you'll get a new error suggesting to remove the now defunct # noqa comment:
ruff unused noqa behavior
$ cat t1.py
def f() -> None:
x: int = 'hi'
$ ruff check --select=F,RUF t1.py
t1.py:2:5: F841 Local variable `x` is assigned to but never used
|
1 | def f() -> None:
2 | x: int = 'hi'
| ^ F841
|
= help: Remove assignment to unused variable `x`
# Add '# noqa' comment
$ cat t2.py
def f() -> None:
x: int = 'hi' # noqa: F841
# Linting now passes
$ ruff check --select=F,RUF t2.py
All checks passed!
# Now disable the rule with '--ignore=F841'
# Ruff now warns you about the unused '# noqa' comment
$ ruff check --select=F,RUF --ignore=F841 t2.py
t2.py:2:20: RUF100 [*] Unused `noqa` directive (non-enabled: `F841`)
|
1 | def f() -> None:
2 | x: int = 'hi' # noqa: F841
| ^^^^^^^^^^^^ RUF100
|
= help: Remove unused `noqa` directive
Found 1 error.
[*] 1 fixable with the `--fix` option.
tagging with unused-ignore doesn't really help though -- since now it could live there ~forever even if the actual thing gets fixed
I don't think ruff's behaviour is particularly relevant -- incremental application isn't a common approach there.
@asottile-sentry how does adding warn_unused_ignores = false to [mypy-t] along with the disable_error_code strike you as a solution (to your particular problem)?
again that defeats the whole point for me
But then the type-ignores won't live forever, only until you remove the pair of configurations from your configuration file.
it won't but it lets other completely wrong type ignores creep in / stick around
I see, that's true.
Besides the obvious solutions (the one I just mentioned, the one brianschubert mentioned, changing the behavior of warn-unused-ignores back, introducing a new configuration warn_unused_ignores_except, & just removing the type ignores), which all have their drawbacks, you could also check out basedmypy's baselining feature https://kotlinisland.github.io/basedmypy/baseline. Basedmypy is a fork of mypy and has slightly different behavior sometimes (and different things enabled by default), but there's a slight chance that it's exactly what you need.
yeah I'm never using anything by that author or with "based" in the name so that's not really a fix to mypy's behaviour change
What if mypy didn't warn about any unused ignores with error codes that are suppressed in that file? It feels worse than previous status quo, but it's also consistent (in that checks that try to save work if disabled also have the right behavior).
(I really don't like that behavior though. But no matter what there's a nonlocal thing affecting the file which is very confusing so I think adding a bit more isn't too bad?)
On the other hand making file-level disabling of error codes different than project-level disabling of error codes feels disappointing. Do you get warned that an ignore is useless if you disable an error code for your whole project?
I still don't see why the original behaviour had to change. I feel like it was pretty good: an error was suppressed on this line so the ignore is valid, it just also happened to be suppressed globally due to reduced strictness.
The main issue with the previous behavior was that it was inconsistent and also not very predictable. It only worked for some errors (not necessarily even for all errors with the same error code), and whether it worked for a particular error could also change unexpectedly when other parts of the configuration changed or between mypy releases.
For example, was this ok under the previous behavior?
# flags: --warn-unused-ignores --disable-error-code no-untyped-def
def foo(x) -> None: pass # type: ignore[no-untyped-def]
Whether this emitted an unused-ignore error or not depended on the values of stict, disallow_untyped_defs, and disallow_incomplete_defs, even though regardless of those settings a no-untyped-def would not be emitted if the type: ignore were removed.
in my mind it would be better to address those inconsistencies rather than what was done which makes incremental adoption much more painful
for instance (guessing how things work) by having mypy always produce the error for untyped / incomplete defs and then use the 1.15 type-ignore behaviour. should be more consistent but without having the undesirable behaviour change
You can find actual instances of this by searching the mypy codebase for is_error_code_enabled. For instance, the first result when I try that locally only actually creates a visitor and uses it if we're checking whether things are possibly defined/want to know if assignment A happens before use B. That visitor seemingly reanalyzes a scope to know specifically when assignments happened! We only want to enable that potentially costly thing if the error code is actually going to be seen. (and we obviously can't know if a statement could trigger that error cheaply, or else we would do that...)
So IMO it's simply impossible to know whether an ignore is actually unused without regard to file configuration without perf downsides (running the file twice? running the file once and filtering out certain error codes? I guess maybe the second would work and it would only be a minor perf change. Probably. There's probably issues due to flags vs error codes, where some flag might increase the number of e.g. misc errors?)