From 6ca3d6fb3d1a6f5466f4dfbc819e0ffd8c27d040 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 30 Jan 2026 01:06:15 -0500 Subject: [PATCH 01/10] added fuzzy matching for name not defined errors --- mypy/semanal.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index 20bcb2f4ac60a..e6bd2d835f69f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7475,6 +7475,12 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None self.record_incomplete_ref() return message = f'Name "{name}" is not defined' + # Collect all names in scope to suggest similar alternatives + alternatives = self._get_names_in_scope() + alternatives.discard(name) + matches = best_matches(name, alternatives, n=3) + if matches: + message += f"; did you mean {pretty_seq(matches, 'or')}?" self.fail(message, ctx, code=codes.NAME_DEFINED) if f"builtins.{name}" in SUGGESTED_TEST_FIXTURES: @@ -7499,6 +7505,36 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None ).format(module=module, name=lowercased[fullname].rsplit(".", 1)[-1]) self.note(hint, ctx, code=codes.NAME_DEFINED) + def _get_names_in_scope(self) -> set[str]: + """Collect all names visible in the current scope for fuzzy matching suggestions. + + This includes: + - Local variables (from function scopes) + - Class attributes (if it's inside a class) + - Global/module-level names + - Builtins + """ + names: set[str] = set() + + for table in self.locals: + if table is not None: + names.update(table.keys()) + + if self.type is not None: + names.update(self.type.names.keys()) + + names.update(self.globals.keys()) + + b = self.globals.get("__builtins__", None) + if b and isinstance(b.node, MypyFile): + # Only include public builtins (not _private ones) + for builtin_name in b.node.names.keys(): + if not (len(builtin_name) > 1 and builtin_name[0] == "_" and builtin_name[1] != "_"): + names.add(builtin_name) + + # Filter out internal/dunder names that aren't useful for suggestions and might introduce noise + return {n for n in names if not n.startswith("__") or n.endswith("__")} + def already_defined( self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None, noun: str ) -> None: From 115b8316b86e30afbf66c5f7d7700393f076caa9 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 30 Jan 2026 01:06:20 -0500 Subject: [PATCH 02/10] added test to validate suggestions --- test-data/unit/check-errorcodes.test | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 58f48144b3e56..eca5a2e687c80 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -21,6 +21,25 @@ def f() -> None: [file m.py] [builtins fixtures/module.pyi] +[case testErrorCodeUndefinedNameSuggestion] +my_variable = 42 +my_constant = 100 + +x = my_variabel # E: Name "my_variabel" is not defined; did you mean "my_variable"? [name-defined] + +def calculate_sum(items: int) -> int: + return items + +calculate_summ(1) # E: Name "calculate_summ" is not defined; did you mean "calculate_sum"? [name-defined] + +class MyClass: + pass + +y = MyClas() # E: Name "MyClas" is not defined; did you mean "MyClass"? [name-defined] + +unknown_xyz # E: Name "unknown_xyz" is not defined [name-defined] +[builtins fixtures/module.pyi] + [case testErrorCodeUnclassifiedError] class A: def __init__(self) -> int: \ From 31d0bd2bfa0ed0c8c0ca301191d6a3695c1fa6fd Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 13 Feb 2026 03:24:02 -0500 Subject: [PATCH 03/10] fix filtering names bug and be more strict on name collection --- mypy/semanal.py | 20 ++++++++++++-------- test-data/unit/pythoneval.test | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index e6bd2d835f69f..e87e9674bfedd 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7475,12 +7475,16 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None self.record_incomplete_ref() return message = f'Name "{name}" is not defined' - # Collect all names in scope to suggest similar alternatives - alternatives = self._get_names_in_scope() - alternatives.discard(name) - matches = best_matches(name, alternatives, n=3) - if matches: - message += f"; did you mean {pretty_seq(matches, 'or')}?" + if ( + "." not in name + and not (name.startswith("__") and name.endswith("__")) + and f"builtins.{name}" not in SUGGESTED_TEST_FIXTURES + ): + alternatives = self._get_names_in_scope() + alternatives.discard(name) + matches = best_matches(name, alternatives, n=3) + if matches: + message += f"; did you mean {pretty_seq(matches, 'or')}?" self.fail(message, ctx, code=codes.NAME_DEFINED) if f"builtins.{name}" in SUGGESTED_TEST_FIXTURES: @@ -7532,8 +7536,8 @@ def _get_names_in_scope(self) -> set[str]: if not (len(builtin_name) > 1 and builtin_name[0] == "_" and builtin_name[1] != "_"): names.add(builtin_name) - # Filter out internal/dunder names that aren't useful for suggestions and might introduce noise - return {n for n in names if not n.startswith("__") or n.endswith("__")} + # Filter out internal/dunder names that aren't useful as suggestions + return {n for n in names if not n.startswith("__")} def already_defined( self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None, noun: str diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 9f057042926d3..6c91d0d2d5ce1 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -608,7 +608,7 @@ def f(x: _T) -> None: pass s: FrozenSet [out] _program.py:2: error: Name "_T" is not defined -_program.py:3: error: Name "FrozenSet" is not defined +_program.py:3: error: Name "FrozenSet" is not defined; did you mean "frozenset"? [case testVarArgsFunctionSubtyping] import typing From 55a8efcefddf65d4c5e440f3c931e15fee7cebe3 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 13 Feb 2026 03:36:59 -0500 Subject: [PATCH 04/10] added prefer_simple_messages feature gate --- mypy/semanal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index e87e9674bfedd..6dec716772aa3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7476,7 +7476,8 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None return message = f'Name "{name}" is not defined' if ( - "." not in name + not self.msg.prefer_simple_messages() + and "." not in name and not (name.startswith("__") and name.endswith("__")) and f"builtins.{name}" not in SUGGESTED_TEST_FIXTURES ): From 1dcdd5f44c567f707ccaceff649970778fe5941c Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Sun, 15 Mar 2026 19:45:41 -0400 Subject: [PATCH 05/10] skip fuzzy matching for ignored errors --- mypy/errors.py | 17 ++++++++++++++ mypy/semanal.py | 6 +++-- test-data/unit/check-errorcodes.test | 33 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index edfb3bd1607a6..9cf9e10c575bf 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -828,6 +828,23 @@ def is_ignored_error(self, line: int, info: ErrorInfo, ignores: dict[int, list[s ) return False + def is_error_code_ignored(self, line: int, code: ErrorCode) -> bool: + """Check if an error with the given code on the given line would be ignored. + + This is useful to skip expensive error message processing (e.g. fuzzy matching) + when the error will be suppressed anyway. + """ + ignores = self.ignored_lines.get(self.file, {}) + if line not in ignores: + return False + if not ignores[line]: + return True + return ( + code.code in ignores[line] + or code.sub_code_of is not None + and code.sub_code_of.code in ignores[line] + ) + def is_error_code_enabled(self, error_code: ErrorCode) -> bool: if self.options: current_mod_disabled = self.options.disabled_error_codes diff --git a/mypy/semanal.py b/mypy/semanal.py index 6dec716772aa3..b4af2acfd7bd8 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7480,6 +7480,7 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None and "." not in name and not (name.startswith("__") and name.endswith("__")) and f"builtins.{name}" not in SUGGESTED_TEST_FIXTURES + and not self.errors.is_error_code_ignored(ctx.line, codes.NAME_DEFINED) ): alternatives = self._get_names_in_scope() alternatives.discard(name) @@ -7515,7 +7516,7 @@ def _get_names_in_scope(self) -> set[str]: This includes: - Local variables (from function scopes) - - Class attributes (if it's inside a class) + - Class attributes (only when directly in class body, not in methods) - Global/module-level names - Builtins """ @@ -7525,7 +7526,8 @@ def _get_names_in_scope(self) -> set[str]: if table is not None: names.update(table.keys()) - if self.type is not None: + if self.is_class_scope(): + assert self.type is not None names.update(self.type.names.keys()) names.update(self.globals.keys()) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index eca5a2e687c80..669d623d09209 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -40,6 +40,39 @@ y = MyClas() # E: Name "MyClas" is not defined; did you mean "MyClass"? [name- unknown_xyz # E: Name "unknown_xyz" is not defined [name-defined] [builtins fixtures/module.pyi] +[case testErrorCodeUndefinedNameSuggestionLocal] +def foo() -> None: + local_value = 10 + x = local_valeu # E: Name "local_valeu" is not defined; did you mean "local_value"? [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionOuterScope] +top_level_var = 42 + +def foo() -> None: + x = top_level_vr # E: Name "top_level_vr" is not defined; did you mean "top_level_var"? [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionClassBody] +class Foo: + class_attr = 10 + x = class_atr # E: Name "class_atr" is not defined; did you mean "class_attr"? [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionFromImport] +from m import some_function + +some_functon(1) # E: Name "some_functon" is not defined; did you mean "some_function"? [name-defined] + +[file m.py] +def some_function(x: int) -> int: + return x +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionBuiltin] +x = lenn # E: Name "lenn" is not defined; did you mean "len"? [name-defined] +[builtins fixtures/len.pyi] + [case testErrorCodeUnclassifiedError] class A: def __init__(self) -> int: \ From ce1d1efb4546bac0027cf8af12013cf3b88c76ad Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Sun, 15 Mar 2026 19:49:05 -0400 Subject: [PATCH 06/10] rename --- mypy/errors.py | 2 +- mypy/semanal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 9cf9e10c575bf..3f844e9a3fff6 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -828,7 +828,7 @@ def is_ignored_error(self, line: int, info: ErrorInfo, ignores: dict[int, list[s ) return False - def is_error_code_ignored(self, line: int, code: ErrorCode) -> bool: + def is_ignored_error_code(self, line: int, code: ErrorCode) -> bool: """Check if an error with the given code on the given line would be ignored. This is useful to skip expensive error message processing (e.g. fuzzy matching) diff --git a/mypy/semanal.py b/mypy/semanal.py index b4af2acfd7bd8..9dc59ef2a25b6 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7480,7 +7480,7 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None and "." not in name and not (name.startswith("__") and name.endswith("__")) and f"builtins.{name}" not in SUGGESTED_TEST_FIXTURES - and not self.errors.is_error_code_ignored(ctx.line, codes.NAME_DEFINED) + and not self.errors.is_ignored_error_code(ctx.line, codes.NAME_DEFINED) ): alternatives = self._get_names_in_scope() alternatives.discard(name) From cae911d7e8510018757cbcc0fdcaeb9c0730ca88 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Sun, 15 Mar 2026 20:00:38 -0400 Subject: [PATCH 07/10] add test for ignored error --- test-data/unit/check-errorcodes.test | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 669d623d09209..442b1aac34e2b 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -73,6 +73,12 @@ def some_function(x: int) -> int: x = lenn # E: Name "lenn" is not defined; did you mean "len"? [name-defined] [builtins fixtures/len.pyi] +[case testErrorCodeUndefinedNameSuggestionIgnored] +my_variable = 42 +x = my_variabel # type: ignore[name-defined] +y = my_variabel # type: ignore +[builtins fixtures/module.pyi] + [case testErrorCodeUnclassifiedError] class A: def __init__(self) -> int: \ From 9d0b73d568e35e6b2de54d816826a9a3e17144ab Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Sun, 15 Mar 2026 22:03:56 -0400 Subject: [PATCH 08/10] add more test cases --- mypy/errors.py | 1 + mypy/semanal.py | 4 ++-- test-data/unit/check-errorcodes.test | 13 +++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 3f844e9a3fff6..e4518f30eefff 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -838,6 +838,7 @@ def is_ignored_error_code(self, line: int, code: ErrorCode) -> bool: if line not in ignores: return False if not ignores[line]: + # Empty list means that we ignore all errors return True return ( code.code in ignores[line] diff --git a/mypy/semanal.py b/mypy/semanal.py index 9dc59ef2a25b6..febcfea864624 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7533,8 +7533,8 @@ def _get_names_in_scope(self) -> set[str]: names.update(self.globals.keys()) b = self.globals.get("__builtins__", None) - if b and isinstance(b.node, MypyFile): - # Only include public builtins (not _private ones) + if b: + assert isinstance(b.node, MypyFile) for builtin_name in b.node.names.keys(): if not (len(builtin_name) > 1 and builtin_name[0] == "_" and builtin_name[1] != "_"): names.add(builtin_name) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 442b1aac34e2b..e4d9f663496a5 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -73,6 +73,19 @@ def some_function(x: int) -> int: x = lenn # E: Name "lenn" is not defined; did you mean "len"? [name-defined] [builtins fixtures/len.pyi] +[case testErrorCodeUndefinedNameSuggestionClassMethod] +class Foo: + class_attr = 10 + def method(self) -> None: + x = class_atr # E: Name "class_atr" is not defined [name-defined] +[builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedNameSuggestionMultiple] +total_count = 1 +total_counts = 2 +x = total_countt # E: Name "total_countt" is not defined; did you mean "total_count" or "total_counts"? [name-defined] +[builtins fixtures/module.pyi] + [case testErrorCodeUndefinedNameSuggestionIgnored] my_variable = 42 x = my_variabel # type: ignore[name-defined] From ba25bd9492eec8221371e55a800327a0cea49ac3 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Mon, 16 Mar 2026 02:05:24 -0400 Subject: [PATCH 09/10] simplified ignored error checking --- mypy/errors.py | 18 ------------------ mypy/semanal.py | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index e4518f30eefff..edfb3bd1607a6 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -828,24 +828,6 @@ def is_ignored_error(self, line: int, info: ErrorInfo, ignores: dict[int, list[s ) return False - def is_ignored_error_code(self, line: int, code: ErrorCode) -> bool: - """Check if an error with the given code on the given line would be ignored. - - This is useful to skip expensive error message processing (e.g. fuzzy matching) - when the error will be suppressed anyway. - """ - ignores = self.ignored_lines.get(self.file, {}) - if line not in ignores: - return False - if not ignores[line]: - # Empty list means that we ignore all errors - return True - return ( - code.code in ignores[line] - or code.sub_code_of is not None - and code.sub_code_of.code in ignores[line] - ) - def is_error_code_enabled(self, error_code: ErrorCode) -> bool: if self.options: current_mod_disabled = self.options.disabled_error_codes diff --git a/mypy/semanal.py b/mypy/semanal.py index febcfea864624..eced1be5a5619 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7480,7 +7480,7 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None and "." not in name and not (name.startswith("__") and name.endswith("__")) and f"builtins.{name}" not in SUGGESTED_TEST_FIXTURES - and not self.errors.is_ignored_error_code(ctx.line, codes.NAME_DEFINED) + and ctx.line not in self.errors.ignored_lines.get(self.errors.file, {}) ): alternatives = self._get_names_in_scope() alternatives.discard(name) From cba38077a019f99f8208cabdd68aaa2a7506cb72 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 06:07:14 +0000 Subject: [PATCH 10/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/semanal.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index eced1be5a5619..283ad57f12def 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7536,7 +7536,9 @@ def _get_names_in_scope(self) -> set[str]: if b: assert isinstance(b.node, MypyFile) for builtin_name in b.node.names.keys(): - if not (len(builtin_name) > 1 and builtin_name[0] == "_" and builtin_name[1] != "_"): + if not ( + len(builtin_name) > 1 and builtin_name[0] == "_" and builtin_name[1] != "_" + ): names.add(builtin_name) # Filter out internal/dunder names that aren't useful as suggestions