From e44d14f8e52a8890d08726ee753c8754edefd649 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Sat, 26 Jul 2025 18:56:37 -0700 Subject: [PATCH 1/5] Bump version to 1.17.1+dev --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index 60b64f938241..a305b237a771 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "1.17.0" +__version__ = "1.17.1+dev" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) From 88fdeaae0abc92d605fc475fd153b4ad5b239310 Mon Sep 17 00:00:00 2001 From: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> Date: Wed, 16 Jul 2025 03:10:02 +0200 Subject: [PATCH 2/5] Prevent a crash when InitVar is redefined with a method in a subclass (#19453) Fixes #19443. This case is too niche (and should be trivially avoidable), so just not crashing should be good enough. The value is indeed redefined, and trying to massage the plugin to move the `X-redefinition` back to `X` in names is not worth the effort IMO. --- mypy/checker.py | 9 ++++++++- test-data/unit/check-dataclasses.test | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 217a4a885dd8..d2da9c51f80b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2169,7 +2169,14 @@ def check_method_override_for_base_with_name( else: override_class_or_static = defn.func.is_class or defn.func.is_static typ, _ = self.node_type_from_base(defn.name, defn.info, defn) - assert typ is not None + if typ is None: + # This may only happen if we're checking `x-redefinition` member + # and `x` itself is for some reason gone. Normally the node should + # be reachable from the containing class by its name. + # The redefinition is never removed, use this as a sanity check to verify + # the reasoning above. + assert f"{defn.name}-redefinition" in defn.info.names + return False original_node = base_attr.node # `original_type` can be partial if (e.g.) it is originally an diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 30d8497c9cd2..90be3707eed4 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2666,3 +2666,19 @@ class PersonBad(TypedDict): class JobBad: person: PersonBad = field(default_factory=PersonBad) # E: Argument "default_factory" to "field" has incompatible type "type[PersonBad]"; expected "Callable[[], PersonBad]" [builtins fixtures/dict.pyi] + +[case testDataclassInitVarRedefinitionNoCrash] +# https://github.com/python/mypy/issues/19443 +from dataclasses import InitVar, dataclass + +class ClassA: + def value(self) -> int: + return 0 + +@dataclass +class ClassB(ClassA): + value: InitVar[int] + + def value(self) -> int: # E: Name "value" already defined on line 10 + return 0 +[builtins fixtures/dict.pyi] From 5f4428f0286df58169d2f34f4f86561ad617538b Mon Sep 17 00:00:00 2001 From: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:41:41 +0200 Subject: [PATCH 3/5] Fix "ignored exception in `hasattr`" in dmypy (#19428) Fixes #19425. That property has no setter so it should safe to exclude. It can raise in inconsistent state that arises when it is not copied last? --- mypy/server/astmerge.py | 15 ++++++++++----- pyproject.toml | 8 ++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index 8cd574628bb8..33e2d2b799cb 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -345,13 +345,11 @@ def visit_type_alias(self, node: TypeAlias) -> None: def fixup(self, node: SN) -> SN: if node in self.replacements: new = self.replacements[node] - skip_slots: tuple[str, ...] = () if isinstance(node, TypeInfo) and isinstance(new, TypeInfo): # Special case: special_alias is not exposed in symbol tables, but may appear # in external types (e.g. named tuples), so we need to update it manually. - skip_slots = ("special_alias",) replace_object_state(new.special_alias, node.special_alias) - replace_object_state(new, node, skip_slots=skip_slots) + replace_object_state(new, node, skip_slots=_get_ignored_slots(new)) return cast(SN, new) return node @@ -556,9 +554,16 @@ def replace_nodes_in_symbol_table( if node.node in replacements: new = replacements[node.node] old = node.node - # Needed for TypeInfo, see comment in fixup() above. - replace_object_state(new, old, skip_slots=("special_alias",)) + replace_object_state(new, old, skip_slots=_get_ignored_slots(new)) node.node = new if isinstance(node.node, (Var, TypeAlias)): # Handle them here just in case these aren't exposed through the AST. node.node.accept(NodeReplaceVisitor(replacements)) + + +def _get_ignored_slots(node: SymbolNode) -> tuple[str, ...]: + if isinstance(node, OverloadedFuncDef): + return ("setter",) + if isinstance(node, TypeInfo): + return ("special_alias",) + return () diff --git a/pyproject.toml b/pyproject.toml index 1870e0931407..032bfcb609e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -222,6 +222,14 @@ addopts = "-nauto --strict-markers --strict-config" # treat xpasses as test failures so they get converted to regular tests as soon as possible xfail_strict = true +# Force warnings as errors +filterwarnings = [ + "error", + # Some testcases may contain code that emits SyntaxWarnings, and they are not yet + # handled consistently in 3.14 (PEP 765) + "default::SyntaxWarning", +] + [tool.coverage.run] branch = true source = ["mypy"] From 933c913fbe6d2fbf277ff8d6b2f2298f0f84be64 Mon Sep 17 00:00:00 2001 From: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> Date: Mon, 21 Jul 2025 20:30:59 +0200 Subject: [PATCH 4/5] Retain `None` as constraints bottom if no bottoms were provided (#19485) Current version replaces `None` (which indicates "no items") with an empty union (=`Uninhabited`). This breaks inference in some cases. Fixes #19483. --- mypy/solve.py | 6 ++++-- test-data/unit/check-recursive-types.test | 4 ++-- test-data/unit/check-typeddict.test | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/mypy/solve.py b/mypy/solve.py index 098d926bc789..fbbcac2520ad 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -270,6 +270,7 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: uppers = new_uppers # ...unless this is the only information we have, then we just pass it on. + lowers = list(lowers) if not uppers and not lowers: candidate = UninhabitedType() candidate.ambiguous = True @@ -281,10 +282,11 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: # Process each bound separately, and calculate the lower and upper # bounds based on constraints. Note that we assume that the constraint # targets do not have constraint references. - if type_state.infer_unions: + if type_state.infer_unions and lowers: # This deviates from the general mypy semantics because # recursive types are union-heavy in 95% of cases. - bottom = UnionType.make_union(list(lowers)) + # Retain `None` when no bottoms were provided to avoid bogus `Never` inference. + bottom = UnionType.make_union(lowers) else: # The order of lowers is non-deterministic. # We attempt to sort lowers because joins are non-associative. For instance: diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 7ed5ea53c27e..86e9f02b5263 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -54,7 +54,7 @@ reveal_type(flatten([1, [2, [3]]])) # N: Revealed type is "builtins.list[builti class Bad: ... x: Nested[int] = [1, [2, [3]]] -x = [1, [Bad()]] # E: List item 1 has incompatible type "list[Bad]"; expected "Union[int, Nested[int]]" +x = [1, [Bad()]] # E: List item 0 has incompatible type "Bad"; expected "Union[int, Nested[int]]" [builtins fixtures/isinstancelist.pyi] [case testRecursiveAliasGenericInferenceNested] @@ -605,7 +605,7 @@ class NT(NamedTuple, Generic[T]): class A: ... class B(A): ... -nti: NT[int] = NT(key=0, value=NT(key=1, value=A())) # E: Argument "value" to "NT" has incompatible type "NT[A]"; expected "Union[int, NT[int]]" +nti: NT[int] = NT(key=0, value=NT(key=1, value=A())) # E: Argument "value" to "NT" has incompatible type "A"; expected "Union[int, NT[int]]" reveal_type(nti) # N: Revealed type is "tuple[builtins.int, Union[builtins.int, ...], fallback=__main__.NT[builtins.int]]" nta: NT[A] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index a068a63274ca..be5a6c655d8c 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -4271,3 +4271,21 @@ reveal_type(dicts.TF) # N: Revealed type is "def (*, user_id: builtins.int =) - reveal_type(dicts.TotalFalse) # N: Revealed type is "def (*, user_id: builtins.int =) -> TypedDict('__main__.Dicts.TF', {'user_id'?: builtins.int})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testRecursiveNestedTypedDictInference] +from typing import TypedDict, Sequence +from typing_extensions import NotRequired + +class Component(TypedDict): + type: str + components: NotRequired[Sequence['Component']] + +inputs: Sequence[Component] = [{ + 'type': 'tuple', + 'components': [ + {'type': 'uint256'}, + {'type': 'address'}, + ] +}] +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] From acb29831e286bbccde37c03bc75381f40a5fdc9e Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Wed, 30 Jul 2025 23:20:43 -0700 Subject: [PATCH 5/5] Bump version to 1.17.1 --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index a305b237a771..d20fa047b8d6 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "1.17.1+dev" +__version__ = "1.17.1" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))