diff --git a/mypy/checker.py b/mypy/checker.py
index 2d82d74cc197..fff601c93909 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -5225,9 +5225,24 @@ def visit_del_stmt(self, s: DelStmt) -> None:
s.expr.accept(self.expr_checker)
for elt in flatten(s.expr):
if isinstance(elt, NameExpr):
- self.binder.assign_type(
- elt, DeletedType(source=elt.name), get_declaration(elt)
- )
+ # For local variables, completely remove type binding to allow reuse
+ if (
+ isinstance(elt.node, Var)
+ and elt.node.is_inferred
+ and not elt.node.is_property
+ and not elt.node.is_classvar
+ and elt.node.is_local
+ ):
+ # Completely remove the variable from type tracking
+ self.binder.cleanse(elt)
+ # Also remove from type map if present
+ if hasattr(self, "type_map") and elt in self.type_map:
+ del self.type_map[elt]
+ else:
+ # For non-local variables, use the existing DeletedType behavior
+ self.binder.assign_type(
+ elt, DeletedType(source=elt.name), get_declaration(elt)
+ )
def visit_decorator(self, e: Decorator) -> None:
for d in e.decorators:
@@ -5306,7 +5321,7 @@ def visit_decorator_inner(
self.fail(message_registry.BAD_CONSTRUCTOR_TYPE, e)
if e.func.original_def and isinstance(sig, FunctionLike):
- # Function definition overrides function definition.
+ # Function signature processing continues herection definition overrides function definition.
self.check_func_def_override(e.func, sig)
def check_for_untyped_decorator(
diff --git a/mypy/html_report.py b/mypy/html_report.py
new file mode 100644
index 000000000000..7fd57046bd84
--- /dev/null
+++ b/mypy/html_report.py
@@ -0,0 +1,278 @@
+"""Classes for producing HTML reports about type checking results."""
+
+from __future__ import annotations
+
+import collections
+import os
+import shutil
+from typing import Any
+
+from mypy import stats
+from mypy.nodes import Expression, MypyFile
+from mypy.options import Options
+from mypy.report import (
+ AbstractReporter,
+ FileInfo,
+ iterate_python_lines,
+ register_reporter,
+ should_skip_path,
+)
+from mypy.types import Type, TypeOfAny
+from mypy.version import __version__
+
+# Map of TypeOfAny enum values to descriptive strings
+type_of_any_name_map = {
+ TypeOfAny.unannotated: "Unannotated",
+ TypeOfAny.explicit: "Explicit",
+ TypeOfAny.from_unimported_type: "Unimported",
+ TypeOfAny.from_omitted_generics: "Omitted Generics",
+ TypeOfAny.from_error: "Error",
+ TypeOfAny.special_form: "Special Form",
+ TypeOfAny.implementation_artifact: "Implementation Artifact",
+}
+
+
+class MemoryHtmlReporter(AbstractReporter):
+ """Internal reporter that generates HTML in memory.
+
+ This is used by the HTML reporter to avoid duplication.
+ """
+
+ def __init__(self, reports: Any, output_dir: str) -> None:
+ super().__init__(reports, output_dir)
+ self.css_html_path = os.path.join(reports.data_dir, "xml", "mypy-html.css")
+ self.last_html: dict[str, str] = {} # Maps file paths to HTML content
+ self.index_html: str | None = None
+ self.files: list[FileInfo] = []
+
+ def on_file(
+ self,
+ tree: MypyFile,
+ modules: dict[str, MypyFile],
+ type_map: dict[Expression, Type],
+ options: Options,
+ ) -> None:
+ try:
+ path = os.path.relpath(tree.path)
+ except ValueError:
+ return
+
+ if should_skip_path(path) or os.path.isdir(path):
+ return # `path` can sometimes be a directory, see #11334
+
+ visitor = stats.StatisticsVisitor(
+ inferred=True,
+ filename=tree.fullname,
+ modules=modules,
+ typemap=type_map,
+ all_nodes=True,
+ )
+ tree.accept(visitor)
+
+ file_info = FileInfo(path, tree._fullname)
+
+ # Generate HTML for this file
+ html_lines = [
+ "",
+ "",
+ "
",
+ " ",
+ " Mypy Report: " + path + "",
+ " ",
+ " ",
+ "",
+ "",
+ f" Mypy Type Check Report for {path}
",
+ " ",
+ " ",
+ " Line | ",
+ " Precision | ",
+ " Code | ",
+ " Notes | ",
+ "
",
+ ]
+
+ for lineno, line_text in iterate_python_lines(path):
+ status = visitor.line_map.get(lineno, stats.TYPE_EMPTY)
+ file_info.counts[status] += 1
+
+ precision = stats.precision_names[status]
+ any_info = self._get_any_info_for_line(visitor, lineno)
+
+ # Escape HTML special characters in the line content
+ content = line_text.rstrip("\n")
+ content = content.replace("&", "&").replace("<", "<").replace(">", ">")
+
+ # Add CSS class based on precision
+ css_class = precision.lower()
+
+ html_lines.append(
+ f" "
+ f"{lineno} | "
+ f"{precision} | "
+ f"{content} | "
+ f"{any_info} | "
+ "
"
+ )
+
+ html_lines.extend(["
", "", ""])
+
+ self.last_html[path] = "\n".join(html_lines)
+ self.files.append(file_info)
+
+ @staticmethod
+ def _get_any_info_for_line(visitor: stats.StatisticsVisitor, lineno: int) -> str:
+ if lineno in visitor.any_line_map:
+ result = "Any Types on this line: "
+ counter: collections.Counter[int] = collections.Counter()
+ for typ in visitor.any_line_map[lineno]:
+ counter[typ.type_of_any] += 1
+ for any_type, occurrences in counter.items():
+ result += f"
{type_of_any_name_map[any_type]} (x{occurrences})"
+ return result
+ else:
+ return ""
+
+ def on_finish(self) -> None:
+ output_files = sorted(self.files, key=lambda x: x.module)
+
+ # Generate index HTML
+ html_lines = [
+ "",
+ "",
+ "",
+ " ",
+ " Mypy Report Index",
+ " ",
+ " ",
+ "",
+ "",
+ " Mypy Type Check Report
",
+ " Generated with mypy " + __version__ + "
",
+ " ",
+ " ",
+ " Module | ",
+ " File | ",
+ " Precise | ",
+ " Imprecise | ",
+ " Any | ",
+ " Empty | ",
+ " Unanalyzed | ",
+ " Total | ",
+ "
",
+ ]
+
+ for file_info in output_files:
+ counts = file_info.counts
+ html_lines.append(
+ f" "
+ f"{file_info.module} | "
+ f"{file_info.name} | "
+ f"{counts[stats.TYPE_PRECISE]} | "
+ f"{counts[stats.TYPE_IMPRECISE]} | "
+ f"{counts[stats.TYPE_ANY]} | "
+ f"{counts[stats.TYPE_EMPTY]} | "
+ f"{counts[stats.TYPE_UNANALYZED]} | "
+ f"{file_info.total()} | "
+ "
"
+ )
+
+ html_lines.extend(["
", "", ""])
+
+ self.index_html = "\n".join(html_lines)
+
+
+class HtmlReporter(AbstractReporter):
+ """Public reporter that exports HTML directly.
+
+ This reporter generates HTML files for each Python module and an index.html file.
+ """
+
+ def __init__(self, reports: Any, output_dir: str) -> None:
+ super().__init__(reports, output_dir)
+
+ memory_reporter = reports.add_report("memory-html", "")
+ assert isinstance(memory_reporter, MemoryHtmlReporter)
+ # The dependency will be called first.
+ self.memory_html = memory_reporter
+
+ def on_file(
+ self,
+ tree: MypyFile,
+ modules: dict[str, MypyFile],
+ type_map: dict[Expression, Type],
+ options: Options,
+ ) -> None:
+ last_html = self.memory_html.last_html
+ if not last_html:
+ return
+
+ path = os.path.relpath(tree.path)
+ if path.startswith("..") or path not in last_html:
+ return
+
+ out_path = os.path.join(self.output_dir, "html", path + ".html")
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
+
+ with open(out_path, "w", encoding="utf-8") as out_file:
+ out_file.write(last_html[path])
+
+ def on_finish(self) -> None:
+ index_html = self.memory_html.index_html
+ if index_html is None:
+ return
+
+ out_path = os.path.join(self.output_dir, "index.html")
+ out_css = os.path.join(self.output_dir, "mypy-html.css")
+
+ with open(out_path, "w", encoding="utf-8") as out_file:
+ out_file.write(index_html)
+
+ # Copy CSS file if it exists
+ if os.path.exists(self.memory_html.css_html_path):
+ shutil.copyfile(self.memory_html.css_html_path, out_css)
+ else:
+ # Create a basic CSS file if the original doesn't exist
+ with open(out_css, "w", encoding="utf-8") as css_file:
+ css_file.write(
+ """
+ body { font-family: Arial, sans-serif; margin: 20px; }
+ h1 { color: #333; }
+ table { border-collapse: collapse; width: 100%; }
+ th { background-color: #f2f2f2; text-align: left; padding: 8px; }
+ td { padding: 8px; border-bottom: 1px solid #ddd; }
+ tr.precise { background-color: #dff0d8; }
+ tr.imprecise { background-color: #fcf8e3; }
+ tr.any { background-color: #f2dede; }
+ tr.empty, tr.unanalyzed { background-color: #f9f9f9; }
+ pre { margin: 0; white-space: pre-wrap; }
+ a { color: #337ab7; text-decoration: none; }
+ a:hover { text-decoration: underline; }
+ """
+ )
+
+ print("Generated HTML report:", os.path.abspath(out_path))
+
+
+# Register the reporters
+register_reporter("memory-html", MemoryHtmlReporter)
+register_reporter("html-direct", HtmlReporter)
diff --git a/mypy/messages.py b/mypy/messages.py
index 2e07d7f63498..18885b49ec29 100644
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -404,24 +404,38 @@ def has_no_attr(
self.unsupported_left_operand(op, original_type, context)
return codes.OPERATOR
elif member == "__neg__":
+ display_type = (
+ self.pretty_callable_or_overload(original_type)
+ if isinstance(original_type, CallableType)
+ else format_type(original_type, self.options)
+ )
self.fail(
- f"Unsupported operand type for unary - ({format_type(original_type, self.options)})",
+ f"Unsupported operand type for unary - ({display_type})",
context,
code=codes.OPERATOR,
)
return codes.OPERATOR
elif member == "__pos__":
+
+ display_type = (
+ self.pretty_callable_or_overload(original_type)
+ if isinstance(original_type, CallableType)
+ else format_type(original_type, self.options)
+ )
self.fail(
- f"Unsupported operand type for unary + ({format_type(original_type, self.options)})",
+ f"Unsupported operand type for unary + ({display_type})",
context,
code=codes.OPERATOR,
)
return codes.OPERATOR
elif member == "__invert__":
+ display_type = (
+ self.pretty_callable_or_overload(original_type)
+ if isinstance(original_type, CallableType)
+ else format_type(original_type, self.options)
+ )
self.fail(
- f"Unsupported operand type for ~ ({format_type(original_type, self.options)})",
- context,
- code=codes.OPERATOR,
+ f"Unsupported operand type for ~ ({display_type})", context, code=codes.OPERATOR
)
return codes.OPERATOR
elif member == "__getitem__":
diff --git a/test-data/unit/check-del-reuse.test b/test-data/unit/check-del-reuse.test
new file mode 100644
index 000000000000..cdbc3b61138f
--- /dev/null
+++ b/test-data/unit/check-del-reuse.test
@@ -0,0 +1,37 @@
+[case testDelVariableReuse]
+def test_del_reuse() -> None:
+ x = 1
+ reveal_type(x) # N: Revealed type is "builtins.int"
+ del x
+ x = "hello"
+ reveal_type(x) # N: Revealed type is "builtins.str"
+
+[case testDelVariableReuseConditional]
+def test_del_conditional() -> None:
+ x = 1
+ if True:
+ del x
+ x = "hello"
+ reveal_type(x) # N: Revealed type is "builtins.str"
+
+[case testDelVariableNotLocal]
+x = 1
+def test_del_global() -> None:
+ global x
+ del x
+ x = "hello" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+
+[case testDelIndexExpr]
+def test_del_index() -> None:
+ d = {"key": 1}
+ del d["key"]
+ d["key"] = "hello" # OK - this should work normally
+
+[case testDelAttribute]
+class C:
+ attr: int = 1
+
+def test_del_attr() -> None:
+ c = C()
+ del c.attr
+ c.attr = "hello" # E: Incompatible types in assignment (expression has type "str", target has type "int")
diff --git a/test-data/unit/check-del-variable-type.test b/test-data/unit/check-del-variable-type.test
new file mode 100644
index 000000000000..49c5c770b5ff
--- /dev/null
+++ b/test-data/unit/check-del-variable-type.test
@@ -0,0 +1,42 @@
+[case testDelVariableResetsType]
+def test_del_resets_type() -> None:
+ x = 42 # x: int
+ del x
+ x = "hello" # Should not error - x can now be str
+ reveal_type(x) # N: Revealed type is "builtins.str"
+
+[case testDelVariableInBranch]
+def test_del_in_branch(cond: bool) -> None:
+ x = 42 # x: int
+ if cond:
+ del x
+ x = "hello" # Should not error in this branch
+ else:
+ reveal_type(x) # N: Revealed type is "builtins.int"
+
+[case testDelVariableNotLocal]
+class C:
+ attr: int = 42
+
+def test_del_not_local() -> None:
+ c = C()
+ del c.attr # Should use DeletedType behavior, not reset
+ # This should still be treated as deleted, not reset
+
+[case testDelVariableExplicitType]
+def test_del_explicit_type() -> None:
+ x: int = 42
+ del x
+ x = "hello" # Should still error - explicit type annotation
+ [out]
+main:4: error: Incompatible types in assignment (expression has type "str", variable has type "int")
+
+[case testDelVariableMultiple]
+def test_del_multiple() -> None:
+ x = 42
+ y = "hello"
+ del x, y
+ x = "world" # Should not error
+ y = 123 # Should not error
+ reveal_type(x) # N: Revealed type is "builtins.str"
+ reveal_type(y) # N: Revealed type is "builtins.int"