Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/source/error_codes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,43 @@ codes prefixed with ``mypy:`` will be validated:
# OK: error code may be used by other type checker.
y = 2 # type: ignore[something-else]

An alternative way to specify mypy-specific type ignores is to use
``# mypy: ignore[one-code, other-code]``. Such form is equivalent to writing
``# type: ignore[mypy:one-code, mypy:other-code]``. You can have both
``# type: ignore[...]`` and ``# mypy: ignore[...]`` on the same line, in this
case the error code lists will be merged.

Note that to allow better interoperability with other tools, mypy recognizes
``# type: ignore`` and ``mypy: ignore`` in any position in a comment, including
after another "magic" comments used by other tools. To temporarily disable
an ignore, place it after an empty (i.e. whitespace only) comment. For example:

.. code-block:: python

from typing import Any

# No error, type ignore is active.
x: str = 1 # noqa # pragma: no cover # type: ignore[assignment]

# Error: Incompatible types in assignment (expression has type "int", variable has type "str")
y: str = 1 ## type: ignore[assignment]

# Error: Incompatible types in assignment (expression has type "int", variable has type "str")
z: str = 1 # # type: ignore[assignment]

The only exception to this logic, is if there is no other code on a line
except for the comment. In this case type ignores preceded by another comment
are disabled. This is done to avoid spurious errors about unused ignores in
a commented out block of code:

.. code-block:: python

# mypy: warn-unused-ignores

x: int = 1
# OK, no unused-ignore error below.
# x = "no way" # type: ignore[assignment]

Enabling/disabling specific error codes globally
------------------------------------------------

Expand Down
3 changes: 3 additions & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
codes.SYNTAX,
)
INVALID_TYPE_IGNORE: Final = ErrorMessage('Invalid "type: ignore" comment', codes.SYNTAX)
NO_PREFIX_IN_MYPY_IGNORE: Final = (
'Error codes in "mypy: ignore" should not be prefixed with "mypy:"'
)
TYPE_COMMENT_SYNTAX_ERROR_VALUE: Final = ErrorMessage(
'Syntax error in type comment "{}"', codes.SYNTAX
)
Expand Down
118 changes: 101 additions & 17 deletions mypy/nativeparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
)
from mypy.errorcodes import error_codes
from mypy.errors import CompileError
from mypy.message_registry import NO_PREFIX_IN_MYPY_IGNORE
from mypy.nodes import (
ARG_KINDS,
ARG_POS,
Expand Down Expand Up @@ -235,14 +236,25 @@ def native_parse(
b,
errors,
ignores,
mypy_ignores,
import_bytes,
is_partial_package,
uses_template_strings,
source_hash,
mypy_comments,
) = parse_to_binary_ast(filename, options, source, skip_function_bodies)

ignores, extra_errors = normalize_error_codes(ignores)
errors.extend(extra_errors)
mypy_ignores, extra_errors = normalize_error_codes(
mypy_ignores, verify_all=True, prohibit_prefix=True
)
errors.extend(extra_errors)

ignores = merge_type_ignores(ignores, mypy_ignores)
ignores, extra_errors = remove_duplicate_codes(ignores)
errors.extend(extra_errors)

node = MypyFile([], [])
node.path = filename
node.raw_data = FileRawData(
Expand All @@ -258,29 +270,89 @@ def native_parse(
return node, errors, ignores


def normalize_error_codes(ignores: TypeIgnores) -> tuple[TypeIgnores, list[ParseError]]:
def normalize_error_codes(
ignores: TypeIgnores, verify_all: bool = False, prohibit_prefix: bool = False
) -> tuple[TypeIgnores, list[ParseError]]:
"""Remove mypy: prefix from error codes (if present)."""
normalized = []
extra_errors: list[ParseError] = []
for line, codes in ignores:
new_codes = []
for c in codes:
if not c.startswith("mypy:"):
new_codes.append(c)
continue
prefixed = c.startswith("mypy:")
c = c.removeprefix("mypy:")
if c in error_codes:
new_codes.append(c)
# Always record unknown codes, so that type: ignore[unknown]
# will not become blanket ignore.
new_codes.append(c)
if (prefixed or verify_all) and c not in error_codes:
extra_errors.append(
{
"line": line,
"column": -1,
"message": f'Unrecognized error code "{c}"',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the old code had the same thing, but I think this should have the original prefix, i.e. if I get an error for # type: ignore[mypy:fake] I expect to get Unrecognized error code "mypy:fake".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually intentional for consistency with how we use stripped/normalized error codes in other error messages. I would keep it as is.

"blocker": False,
"code": "misc",
}
)
if prefixed and prohibit_prefix:
extra_errors.append(
{
"line": line,
"column": -1,
"message": NO_PREFIX_IN_MYPY_IGNORE,
"blocker": False,
"code": "misc",
}
)
normalized.append((line, new_codes))
return normalized, extra_errors


def merge_type_ignores(ignores: TypeIgnores, mypy_ignores: TypeIgnores) -> TypeIgnores:
"""Merge error codes specified in # type: ignore[...] and in # mypy: ignore[...]."""
merged = []
ignores_by_line = dict(ignores)
mypy_ignores_by_line = dict(mypy_ignores)
for line in sorted(ignores_by_line.keys() | mypy_ignores_by_line.keys()):
codes = ignores_by_line.get(line)
mypy_codes = mypy_ignores_by_line.get(line)
if codes is not None and mypy_codes is None:

@A5rocks A5rocks Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't really know the parser and taking a look didn't really help. Does None mean # type: ignore? If so, I imagine the following blanket case should also apply here!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None means not present on this line.

new_codes = codes
elif codes is None and mypy_codes is not None:
new_codes = mypy_codes
else:
assert codes is not None and mypy_codes is not None
if not codes or not mypy_codes:
# If at least one ignore is blanket, make the merged ignore blanket.
# We are not overly strict here, in case someone wants to quickly
# silence everything.
new_codes = []
else:
new_codes = codes + mypy_codes
merged.append((line, new_codes))
return merged


def remove_duplicate_codes(ignores: TypeIgnores) -> tuple[TypeIgnores, list[ParseError]]:
normalized = []
extra_errors: list[ParseError] = []
for line, codes in ignores:
seen = set()
new_codes = []
for c in codes:
if c in seen:
extra_errors.append(
{
"line": line,
"column": -1,
"message": f'Duplicate error code "{c}"',
"blocker": False,
"code": "misc",
}
)
continue
extra_errors.append(
{
"line": line,
"column": -1,
"message": f'Unrecognized error code "{c}"',
"blocker": False,
"code": "misc",
}
)
seen.add(c)
new_codes.append(c)
normalized.append((line, new_codes))
return normalized, extra_errors

Expand Down Expand Up @@ -327,7 +399,17 @@ def parse_to_binary_ast(
options: Options,
source: str | bytes | None = None,
skip_function_bodies: bool = False,
) -> tuple[bytes, list[ParseError], TypeIgnores, bytes, bool, bool, str, list[tuple[int, str]]]:
) -> tuple[
bytes,
list[ParseError],
TypeIgnores,
TypeIgnores,
bytes,
bool,
bool,
str,
list[tuple[int, str]],
]:
try:
ast_bytes, errors, ignores, import_bytes, ast_data = ast_serialize.parse(
filename,
Expand All @@ -337,7 +419,7 @@ def parse_to_binary_ast(
platform=options.platform,
always_true=options.always_true,
always_false=options.always_false,
cache_version=5,
cache_version=7,
)
except ValueError as exc:
if source is None:
Expand Down Expand Up @@ -366,6 +448,7 @@ def parse_to_binary_ast(
ast_bytes,
errors,
ignores,
ast_data["mypy_ignores"],
import_bytes,
ast_data["is_partial_package"],
ast_data["uses_template_strings"],
Expand Down Expand Up @@ -796,6 +879,7 @@ def read_func_def(state: State, data: ReadBuffer) -> FuncDef:
func_def = FuncDef(name, arguments, body, typ=typ, type_args=type_params)
if is_async:
func_def.is_coroutine = True
func_def.has_type_comments = read_bool(data)
read_loc(data, func_def)
if type_params:
state.check_min_version(
Expand Down
4 changes: 4 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
"docstring",
"deprecated",
"original_first_arg",
"has_type_comments",
)

__match_args__ = ("name", "arguments", "type", "body")
Expand Down Expand Up @@ -1156,6 +1157,9 @@ def __init__(
# We record this status to avoid multiple (similar but different) errors in case
# of partial types etc.
self.is_invalid_redefinition = False
# Record if the function definition has any type comments (including per-argument).
# This is only available with the native parser (and always False otherwise).
self.has_type_comments = False

@property
def name(self) -> str:
Expand Down
1 change: 1 addition & 0 deletions mypy/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def load_from_raw(
# Report parse errors, this replicates the logic in parse().
all_errors = raw_data.raw_errors + state.errors
errors.set_file(fnam, module, options=options)
errors.set_file_ignored_lines(fnam, tree.ignored_lines, options.ignore_errors)
for error in all_errors:
# Note we never raise in this function, so it should not be called in coordinator.
report_parse_error(error, errors)
Expand Down
6 changes: 3 additions & 3 deletions mypy/test/test_nativeparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,15 +268,15 @@ def locs(start_line: int, start_column: int, end_line: int, end_column: int) ->

def test_trivial_binary_data_from_file(self) -> None:
with temp_source("print('hello')") as fnam:
b, _, _, _, _, _, _, _ = parse_to_binary_ast(fnam, Options())
b, _, _, _, _, _, _, _, _ = parse_to_binary_ast(fnam, Options())
self._assert_trivial_binary_data(b)

def test_trivial_binary_data_from_string_source(self) -> None:
b, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), "print('hello')")
b, _, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), "print('hello')")
self._assert_trivial_binary_data(b)

def test_trivial_binary_data_from_bytes_source(self) -> None:
b, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), b"print('hello')")
b, _, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), b"print('hello')")
self._assert_trivial_binary_data(b)

def test_invalid_bytes_raises(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-deprecated.test
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ from typing_extensions import deprecated
@deprecated("use f2 instead")
def f() -> None: ...

f # E: function __main__.f is deprecated: use f2 instead # type: ignore[deprecated]
f # E: function __main__.f is deprecated: use f2 instead
f(1) # E: function __main__.f is deprecated: use f2 instead \
# E: Too many arguments for "f"
f[1] # E: function __main__.f is deprecated: use f2 instead \
Expand Down
3 changes: 2 additions & 1 deletion test-data/unit/check-fastparse.test
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ for i in (1, 2, 3, 100): # type: str # type: ignore
pass
[builtins fixtures/tuple.pyi]

[case testFastParseVariableCommentThenIgnore]
-- New parser has a different stance on this.
[case testFastParseVariableCommentThenIgnore_old_parser]
a="test" # type: int #comment # type: ignore # E: Incompatible types in assignment (expression has type "str", variable has type "int")

[case testFastParseProperty]
Expand Down
93 changes: 93 additions & 0 deletions test-data/unit/check-ignore.test
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,96 @@ x = 1 # type: ignore[mypy:foobar]
main:2: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:2: note: Error code "assignment" not covered by "type: ignore[arg-type, other:foobar]" comment
main:3: error: Unrecognized error code "foobar"
main:3: note: Error code "misc" not covered by "type: ignore[foobar]" comment
main:3: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:3: note: Error code "assignment" not covered by "type: ignore[foobar]" comment

[case testIgnoreNonBlockingParseError]
# flags: --python-version=3.10
m[*(), :] # type: ignore[syntax]
[builtins fixtures/tuple.pyi]
[out]
main:2: error: Name "m" is not defined
main:2: note: Error code "name-defined" not covered by "type: ignore[syntax]" comment

[case testIgnoreNonBlockingParseErrorSilent]
# cmd: mypy -m a
# flags: --python-version=3.10 --follow-import=silent
[file a.py]
import m
reveal_type(m.x) # N: Revealed type is "builtins.int"
[file m.py]
x: int
x = m[*(), :]
[builtins fixtures/tuple.pyi]
[out]

[case testIgnoresWorkAfterOtherComments]
x: str = 1 # noqa # pragma: no cover # type: ignore
y: str = 1 # noqa # pragma: no cover # mypy: ignore
z: str = 1 # noqa # pragma: no cover # type: ignore[assignment]
t: str = 1 # noqa # pragma: no cover # mypy: ignore[assignment]

x1: str = 1 ## type: ignore
y1: str = 1 ## mypy: ignore
z1: str = 1 ## type: ignore[assignment]
t1: str = 1 ## mypy: ignore[assignment]

x2: str = 1 # noqa ## type: ignore
y2: str = 1 # noqa ## mypy: ignore
z2: str = 1 # noqa ## type: ignore[assignment]
t2: str = 1 # noqa ## mypy: ignore[assignment]
[out]
main:6: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:7: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:8: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:9: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:11: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:12: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:13: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:14: error: Incompatible types in assignment (expression has type "int", variable has type "str")

[case testPrefixInMypyCommentNotAllowed]
x: str = 1 # mypy: ignore[mypy:assignment]
[out]
main:1: error: Error codes in "mypy: ignore" should not be prefixed with "mypy:"
main:1: note: Error code "misc" not covered by "type: ignore[assignment]" comment

[case testDuplicateErrorCodeNotAllowed]
x: str = 1 # mypy: ignore[assignment] # type: ignore[assignment]
[out]
main:1: error: Duplicate error code "assignment"
main:1: note: Error code "misc" not covered by "type: ignore[assignment]" comment

[case testUnusedMypyIgnoreReported]
# flags: --warn-unused-ignores
x = 1 # mypy: ignore
y = 1 # mypy: ignore[assignment]
z: str = 1 # mypy: ignore[assignment, arg-type]
[out]
main:2: error: Unused "type: ignore" comment
main:3: error: Unused "type: ignore" comment
main:4: error: Unused "type: ignore[arg-type]" comment

[case testNoUnusedIgnoreInCommentedBlock]
# flags: --warn-unused-ignores
# x: str = 1 # mypy: ignore
# y: str = 1 # type: ignore
# z: str = 1 # mypy: ignore[assignment]
# t: str = 1 # type: ignore[assignment]

[case testIgnoreErrorCodesMerged]
def f() -> None: pass

f(x) # mypy: ignore[name-defined] # type: ignore[call-arg]
f(x) # type: ignore[name-defined] # mypy: ignore[call-arg]

[case testBlanketIgnoreWinsWhenMixed]
x # mypy: ignore # type: ignore[assignment]
y # mypy: ignore[assignment] # type: ignore

[case testUnknownErrorCodeNotAllowedInMypyIgnore]
1 # mypy: ignore[whatever]
[out]
main:1: error: Unrecognized error code "whatever"
main:1: note: Error code "misc" not covered by "type: ignore[whatever]" comment
Loading