From 3010efc873f0dfd0eef2c971feee1f9cc2ce97d8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 24 Mar 2025 20:08:57 +0000 Subject: [PATCH 01/20] Consolidate descriptor handling in checkmember.py (#18831) This is not a pure refactoring, but almost. Right now we are in a weird situation where we have two inconsistencies: * `__set__()` is handled in `checker.py` while `__get__()` is handled in `checkmember.py` * rules for when to use binder are slightly different between descriptors and settable properties. This PR fixes these two things. As a nice bonus we should get free support for unions in `__set__()`. --- mypy/checker.py | 24 ++++++--- mypy/checkexpr.py | 10 +++- mypy/checkmember.py | 126 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 133 insertions(+), 27 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index ac4b24709783..fb435870df8c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3170,7 +3170,7 @@ def check_assignment( ) else: self.try_infer_partial_generic_type_from_assignment(lvalue, rvalue, "=") - lvalue_type, index_lvalue, inferred = self.check_lvalue(lvalue) + lvalue_type, index_lvalue, inferred = self.check_lvalue(lvalue, rvalue) # If we're assigning to __getattr__ or similar methods, check that the signature is # valid. if isinstance(lvalue, NameExpr) and lvalue.node: @@ -4263,7 +4263,9 @@ def check_multi_assignment_from_iterable( else: self.msg.type_not_iterable(rvalue_type, context) - def check_lvalue(self, lvalue: Lvalue) -> tuple[Type | None, IndexExpr | None, Var | None]: + def check_lvalue( + self, lvalue: Lvalue, rvalue: Expression | None = None + ) -> tuple[Type | None, IndexExpr | None, Var | None]: lvalue_type = None index_lvalue = None inferred = None @@ -4281,7 +4283,7 @@ def check_lvalue(self, lvalue: Lvalue) -> tuple[Type | None, IndexExpr | None, V elif isinstance(lvalue, IndexExpr): index_lvalue = lvalue elif isinstance(lvalue, MemberExpr): - lvalue_type = self.expr_checker.analyze_ordinary_member_access(lvalue, True) + lvalue_type = self.expr_checker.analyze_ordinary_member_access(lvalue, True, rvalue) self.store_type(lvalue, lvalue_type) elif isinstance(lvalue, NameExpr): lvalue_type = self.expr_checker.analyze_ref_expr(lvalue, lvalue=True) @@ -4552,12 +4554,8 @@ def check_member_assignment( Return the inferred rvalue_type, inferred lvalue_type, and whether to use the binder for this assignment. - - Note: this method exists here and not in checkmember.py, because we need to take - care about interaction between binder and __set__(). """ instance_type = get_proper_type(instance_type) - attribute_type = get_proper_type(attribute_type) # Descriptors don't participate in class-attribute access if (isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or isinstance( instance_type, TypeType @@ -4569,8 +4567,8 @@ def check_member_assignment( get_lvalue_type = self.expr_checker.analyze_ordinary_member_access( lvalue, is_lvalue=False ) - use_binder = is_same_type(get_lvalue_type, attribute_type) +<<<<<<< HEAD if not isinstance(attribute_type, Instance): # TODO: support __set__() for union types. rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) @@ -4664,13 +4662,23 @@ def check_member_assignment( return AnyType(TypeOfAny.from_error), get_type, False set_type = inferred_dunder_set_type.arg_types[1] +======= +>>>>>>> df9ddfcac (Consolidate descriptor handling in checkmember.py (#18831)) # Special case: if the rvalue_type is a subtype of both '__get__' and '__set__' types, # and '__get__' type is narrower than '__set__', then we invoke the binder to narrow type # by this assignment. Technically, this is not safe, but in practice this is # what a user expects. +<<<<<<< HEAD rvalue_type = self.check_simple_assignment(set_type, rvalue, context) infer = is_subtype(rvalue_type, get_type) and is_subtype(get_type, set_type) return rvalue_type if infer else set_type, get_type, infer +======= + rvalue_type, _ = self.check_simple_assignment(attribute_type, rvalue, context) + infer = is_subtype(rvalue_type, get_lvalue_type) and is_subtype( + get_lvalue_type, attribute_type + ) + return rvalue_type if infer else attribute_type, attribute_type, infer +>>>>>>> df9ddfcac (Consolidate descriptor handling in checkmember.py (#18831)) def check_indexed_assignment( self, lvalue: IndexExpr, rvalue: Expression, context: Context diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1017009ce7ab..3fa4df2a7171 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3327,8 +3327,13 @@ def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: self.chk.warn_deprecated(e.node, e) return narrowed - def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type: - """Analyse member expression or member lvalue.""" + def analyze_ordinary_member_access( + self, e: MemberExpr, is_lvalue: bool, rvalue: Expression | None = None + ) -> Type: + """Analyse member expression or member lvalue. + + An rvalue can be provided optionally to infer better setter type when is_lvalue is True. + """ if e.kind is not None: # This is a reference to a module attribute. return self.analyze_ref_expr(e) @@ -3360,6 +3365,7 @@ def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type in_literal_context=self.is_literal_context(), module_symbol_table=module_symbol_table, is_self=is_self, + rvalue=rvalue, ) return member_type diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 0994d0df400b..9b7c8027e1ed 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -23,6 +23,7 @@ ArgKind, Context, Decorator, + Expression, FuncBase, FuncDef, IndexExpr, @@ -101,6 +102,7 @@ def __init__( module_symbol_table: SymbolTable | None = None, no_deferral: bool = False, is_self: bool = False, + rvalue: Expression | None = None, ) -> None: self.is_lvalue = is_lvalue self.is_super = is_super @@ -113,6 +115,9 @@ def __init__( self.module_symbol_table = module_symbol_table self.no_deferral = no_deferral self.is_self = is_self + if rvalue is not None: + assert is_lvalue + self.rvalue = rvalue def named_type(self, name: str) -> Instance: return self.chk.named_type(name) @@ -139,6 +144,7 @@ def copy_modified( self_type=self.self_type, module_symbol_table=self.module_symbol_table, no_deferral=self.no_deferral, + rvalue=self.rvalue, ) if messages is not None: mx.msg = messages @@ -168,6 +174,7 @@ def analyze_member_access( module_symbol_table: SymbolTable | None = None, no_deferral: bool = False, is_self: bool = False, + rvalue: Expression | None = None, ) -> Type: """Return the type of attribute 'name' of 'typ'. @@ -186,11 +193,14 @@ def analyze_member_access( of 'original_type'. 'original_type' is always preserved as the 'typ' type used in the initial, non-recursive call. The 'self_type' is a component of 'original_type' to which generic self should be bound (a narrower type that has a fallback to instance). - Currently this is used only for union types. + Currently, this is used only for union types. - 'module_symbol_table' is passed to this function if 'typ' is actually a module + 'module_symbol_table' is passed to this function if 'typ' is actually a module, and we want to keep track of the available attributes of the module (since they are not available via the type object directly) + + 'rvalue' can be provided optionally to infer better setter type when is_lvalue is True, + most notably this helps for descriptors with overloaded __set__() method. """ mx = MemberContext( is_lvalue=is_lvalue, @@ -204,6 +214,7 @@ def analyze_member_access( module_symbol_table=module_symbol_table, no_deferral=no_deferral, is_self=is_self, + rvalue=rvalue, ) result = _analyze_member_access(name, typ, mx, override_info) possible_literal = get_proper_type(result) @@ -629,9 +640,7 @@ def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Cont msg.cant_assign_to_final(name, attr_assign=True, ctx=ctx) -def analyze_descriptor_access( - descriptor_type: Type, mx: MemberContext, *, assignment: bool = False -) -> Type: +def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type: """Type check descriptor access. Arguments: @@ -639,7 +648,7 @@ def analyze_descriptor_access( (the type of ``f`` in ``a.f`` when ``f`` is a descriptor). mx: The current member access context. Return: - The return type of the appropriate ``__get__`` overload for the descriptor. + The return type of the appropriate ``__get__/__set__`` overload for the descriptor. """ instance_type = get_proper_type(mx.self_type) orig_descriptor_type = descriptor_type @@ -648,15 +657,24 @@ def analyze_descriptor_access( if isinstance(descriptor_type, UnionType): # Map the access over union types return make_simplified_union( - [ - analyze_descriptor_access(typ, mx, assignment=assignment) - for typ in descriptor_type.items - ] + [analyze_descriptor_access(typ, mx) for typ in descriptor_type.items] ) elif not isinstance(descriptor_type, Instance): return orig_descriptor_type - if not descriptor_type.type.has_readable_member("__get__"): + if not mx.is_lvalue and not descriptor_type.type.has_readable_member("__get__"): + return orig_descriptor_type + + # We do this check first to accommodate for descriptors with only __set__ method. + # If there is no __set__, we type-check that the assigned value matches + # the return type of __get__. This doesn't match the python semantics, + # (which allow you to override the descriptor with any value), but preserves + # the type of accessing the attribute (even after the override). + if mx.is_lvalue and descriptor_type.type.has_readable_member("__set__"): + return analyze_descriptor_assign(descriptor_type, mx) + + if mx.is_lvalue and not descriptor_type.type.has_readable_member("__get__"): + # This turned out to be not a descriptor after all. return orig_descriptor_type dunder_get = descriptor_type.type.get_method("__get__") @@ -713,11 +731,10 @@ def analyze_descriptor_access( callable_name=callable_name, ) - if not assignment: - mx.chk.check_deprecated(dunder_get, mx.context) - mx.chk.warn_deprecated_overload_item( - dunder_get, mx.context, target=inferred_dunder_get_type, selftype=descriptor_type - ) + mx.chk.check_deprecated(dunder_get, mx.context) + mx.chk.warn_deprecated_overload_item( + dunder_get, mx.context, target=inferred_dunder_get_type, selftype=descriptor_type + ) inferred_dunder_get_type = get_proper_type(inferred_dunder_get_type) if isinstance(inferred_dunder_get_type, AnyType): @@ -736,6 +753,79 @@ def analyze_descriptor_access( return inferred_dunder_get_type.ret_type +def analyze_descriptor_assign(descriptor_type: Instance, mx: MemberContext) -> Type: + instance_type = get_proper_type(mx.self_type) + dunder_set = descriptor_type.type.get_method("__set__") + if dunder_set is None: + mx.chk.fail( + message_registry.DESCRIPTOR_SET_NOT_CALLABLE.format( + descriptor_type.str_with_options(mx.msg.options) + ), + mx.context, + ) + return AnyType(TypeOfAny.from_error) + + bound_method = analyze_decorator_or_funcbase_access( + defn=dunder_set, + itype=descriptor_type, + name="__set__", + mx=mx.copy_modified(is_lvalue=False, self_type=descriptor_type), + ) + typ = map_instance_to_supertype(descriptor_type, dunder_set.info) + dunder_set_type = expand_type_by_instance(bound_method, typ) + + callable_name = mx.chk.expr_checker.method_fullname(descriptor_type, "__set__") + rvalue = mx.rvalue or TempNode(AnyType(TypeOfAny.special_form), context=mx.context) + dunder_set_type = mx.chk.expr_checker.transform_callee_type( + callable_name, + dunder_set_type, + [TempNode(instance_type, context=mx.context), rvalue], + [ARG_POS, ARG_POS], + mx.context, + object_type=descriptor_type, + ) + + # For non-overloaded setters, the result should be type-checked like a regular assignment. + # Hence, we first only try to infer the type by using the rvalue as type context. + type_context = rvalue + with mx.msg.filter_errors(): + _, inferred_dunder_set_type = mx.chk.expr_checker.check_call( + dunder_set_type, + [TempNode(instance_type, context=mx.context), type_context], + [ARG_POS, ARG_POS], + mx.context, + object_type=descriptor_type, + callable_name=callable_name, + ) + + # And now we in fact type check the call, to show errors related to wrong arguments + # count, etc., replacing the type context for non-overloaded setters only. + inferred_dunder_set_type = get_proper_type(inferred_dunder_set_type) + if isinstance(inferred_dunder_set_type, CallableType): + type_context = TempNode(AnyType(TypeOfAny.special_form), context=mx.context) + mx.chk.expr_checker.check_call( + dunder_set_type, + [TempNode(instance_type, context=mx.context), type_context], + [ARG_POS, ARG_POS], + mx.context, + object_type=descriptor_type, + callable_name=callable_name, + ) + + # Search for possible deprecations: + mx.chk.check_deprecated(dunder_set, mx.context) + mx.chk.warn_deprecated_overload_item( + dunder_set, mx.context, target=inferred_dunder_set_type, selftype=descriptor_type + ) + + # In the following cases, a message already will have been recorded in check_call. + if (not isinstance(inferred_dunder_set_type, CallableType)) or ( + len(inferred_dunder_set_type.arg_types) < 2 + ): + return AnyType(TypeOfAny.from_error) + return inferred_dunder_set_type.arg_types[1] + + def is_instance_var(var: Var) -> bool: """Return if var is an instance variable according to PEP 526.""" return ( @@ -820,6 +910,7 @@ def analyze_var( # A property cannot have an overloaded type => the cast is fine. assert isinstance(expanded_signature, CallableType) if var.is_settable_property and mx.is_lvalue and var.setter_type is not None: + # TODO: use check_call() to infer better type, same as for __set__(). result = expanded_signature.arg_types[0] else: result = expanded_signature.ret_type @@ -832,7 +923,7 @@ def analyze_var( result = AnyType(TypeOfAny.special_form) fullname = f"{var.info.fullname}.{name}" hook = mx.chk.plugin.get_attribute_hook(fullname) - if result and not mx.is_lvalue and not implicit: + if result and not (implicit or var.info.is_protocol and is_instance_var(var)): result = analyze_descriptor_access(result, mx) if hook: result = hook( @@ -1106,6 +1197,7 @@ def analyze_class_attribute_access( result = add_class_tvars( t, isuper, is_classmethod, is_staticmethod, mx.self_type, original_vars=original_vars ) + # __set__ is not called on class objects. if not mx.is_lvalue: result = analyze_descriptor_access(result, mx) From 8e7c094a409f766395f2c4c31e0edbf99614df31 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 30 Apr 2025 10:20:34 +0100 Subject: [PATCH 02/20] Local forward refs should precede global forward refs (#19000) Fixes https://github.com/python/mypy/issues/18988 This should be a minimal change to restore backwards compatibility for an edge case with forward references. --- mypy/semanal.py | 9 ++++++++ test-data/unit/check-python312.test | 36 ++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index a0cfdcce1e33..a3e113252ac3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6336,6 +6336,8 @@ class C: if node.name not in self.globals: return True global_node = self.globals[node.name] + if not self.is_textually_before_class(global_node.node): + return True return not self.is_type_like(global_node.node) return False @@ -6363,6 +6365,13 @@ def is_textually_before_statement(self, node: SymbolNode) -> bool: else: return line_diff > 0 + def is_textually_before_class(self, node: SymbolNode | None) -> bool: + """Similar to above, but check if a node is defined before current class.""" + assert self.type is not None + if node is None: + return False + return node.line < self.type.defn.line + def is_overloaded_item(self, node: SymbolNode, statement: Statement) -> bool: """Check whether the function belongs to the overloaded variants""" if isinstance(node, OverloadedFuncDef) and isinstance(statement, FuncDef): diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index ba4104a50048..2244548ea969 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -2004,15 +2004,18 @@ reveal_type(x.related_resources) # N: Revealed type is "__main__.ResourceRule" [case testPEP695TypeAliasRecursiveOuterClass] class A: - type X = X + type X = X # E: Cannot resolve name "X" (possible cyclic definition) class X: ... +class AA: + XX = XX # OK, we allow this as a special case. +class XX: ... + class Y: ... class B: type Y = Y -x: A.X -reveal_type(x) # N: Revealed type is "__main__.X" +reveal_type(AA.XX) # N: Revealed type is "def () -> __main__.XX" y: B.Y reveal_type(y) # N: Revealed type is "__main__.Y" [builtins fixtures/tuple.pyi] @@ -2029,3 +2032,30 @@ def foo() -> None: class Z: ... # E: Name "Z" already defined on line 2 [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] + +[case testPEP695MultipleUnpacksInBareApplicationNoCrash] +# https://github.com/python/mypy/issues/18856 +class A[*Ts]: ... + +A[*tuple[int, ...], *tuple[int, ...]] # E: More than one Unpack in a type is not allowed +a: A[*tuple[int, ...], *tuple[int, ...]] # E: More than one Unpack in a type is not allowed +def foo(a: A[*tuple[int, ...], *tuple[int, ...]]): ... # E: More than one Unpack in a type is not allowed + +tuple[*tuple[int, ...], *tuple[int, ...]] # E: More than one Unpack in a type is not allowed +b: tuple[*tuple[int, ...], *tuple[int, ...]] # E: More than one Unpack in a type is not allowed +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testForwardNestedPrecedesForwardGlobal] +from typing import NewType + +class W[T]: pass + +class R: + class M(W[Action.V], type): + FOO = R.Action.V(0) + class Action(metaclass=M): + V = NewType('V', int) + +class Action: + pass From ea8aacd27d8d7252ee88b5715889be03e0e88c3f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 30 Apr 2025 16:35:27 +0100 Subject: [PATCH 03/20] Do not narrow types to Never with binder (#18972) Fixes https://github.com/python/mypy/issues/18967 Fixes https://github.com/python/mypy/issues/16494 Fixes https://github.com/python/mypy/issues/15793 Fixes https://github.com/python/mypy/issues/12949 As you can see from updated test cases, it is kind of gray area, so whether we go this way will depend on the `mypy_primer` results (and also potentially on Dropbox internal code bases, where the above issue may cause problems). --- mypy/checkexpr.py | 8 +++++++- test-data/unit/check-isinstance.test | 4 ++-- test-data/unit/check-narrowing.test | 13 ++++++++++++- test-data/unit/check-python310.test | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3fa4df2a7171..008e056f4367 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6277,7 +6277,13 @@ def narrow_type_from_binder( known_type, restriction, prohibit_none_typevar_overlap=True ): return None - return narrow_declared_type(known_type, restriction) + narrowed = narrow_declared_type(known_type, restriction) + if isinstance(get_proper_type(narrowed), UninhabitedType): + # If we hit this case, it means that we can't reliably mark the code as + # unreachable, but the resulting type can't be expressed in type system. + # Falling back to restriction is more intuitive in most cases. + return restriction + return narrowed return known_type def has_abstract_type_part(self, caller_type: ProperType, callee_type: ProperType) -> bool: diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 49140bf52b8d..058db1ea8197 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1812,9 +1812,9 @@ reveal_type(fm) # N: Revealed type is "__main__.FooMetaclass" if issubclass(fm, Foo): reveal_type(fm) # N: Revealed type is "Type[__main__.Foo]" if issubclass(fm, Bar): - reveal_type(fm) # N: Revealed type is "Never" + reveal_type(fm) # N: Revealed type is "Type[__main__.Bar]" if issubclass(fm, Baz): - reveal_type(fm) # N: Revealed type is "Never" + reveal_type(fm) # N: Revealed type is "Type[__main__.Baz]" [builtins fixtures/isinstance.pyi] [case testIsinstanceAndNarrowTypeVariable] diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 1856ca26f736..dc2cfd46d9ad 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -1284,7 +1284,7 @@ def f(t: Type[T], a: A, b: B) -> None: reveal_type(a) # N: Revealed type is "__main__.A" if type(b) is t: - reveal_type(b) # N: Revealed type is "Never" + reveal_type(b) # N: Revealed type is "T`-1" else: reveal_type(b) # N: Revealed type is "__main__.B" @@ -2413,3 +2413,14 @@ def foo(x: T) -> T: reveal_type(x) # N: Revealed type is "T`-1" return x [builtins fixtures/isinstance.pyi] + +[case testDoNotNarrowToNever] +def any(): + return 1 + +def f() -> None: + x = "a" + x = any() + assert isinstance(x, int) + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/isinstance.pyi] diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 016f50552a5f..a25a7b7107c7 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1299,7 +1299,7 @@ m: str match m: case a if a := 1: # E: Incompatible types in assignment (expression has type "int", variable has type "str") - reveal_type(a) # N: Revealed type is "Never" + reveal_type(a) # N: Revealed type is "Literal[1]?" [case testMatchAssigningPatternGuard] m: str From 0cd434f821eb951283e400a6b93592b1b61daa0f Mon Sep 17 00:00:00 2001 From: Jared Hance Date: Fri, 9 May 2025 17:50:49 -0700 Subject: [PATCH 04/20] fix --- mypy/checker.py | 104 +----------------------------------------------- 1 file changed, 1 insertion(+), 103 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index fb435870df8c..c899acfe7cba 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4568,117 +4568,15 @@ def check_member_assignment( lvalue, is_lvalue=False ) -<<<<<<< HEAD - if not isinstance(attribute_type, Instance): - # TODO: support __set__() for union types. - rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) - return rvalue_type, attribute_type, use_binder - - mx = MemberContext( - is_lvalue=False, - is_super=False, - is_operator=False, - original_type=instance_type, - context=context, - self_type=None, - msg=self.msg, - chk=self, - ) - get_type = analyze_descriptor_access(attribute_type, mx, assignment=True) - if not attribute_type.type.has_readable_member("__set__"): - # If there is no __set__, we type-check that the assigned value matches - # the return type of __get__. This doesn't match the python semantics, - # (which allow you to override the descriptor with any value), but preserves - # the type of accessing the attribute (even after the override). - rvalue_type = self.check_simple_assignment(get_type, rvalue, context) - return rvalue_type, get_type, use_binder - - dunder_set = attribute_type.type.get_method("__set__") - if dunder_set is None: - self.fail( - message_registry.DESCRIPTOR_SET_NOT_CALLABLE.format( - attribute_type.str_with_options(self.options) - ), - context, - ) - return AnyType(TypeOfAny.from_error), get_type, False - - bound_method = analyze_decorator_or_funcbase_access( - defn=dunder_set, - itype=attribute_type, - name="__set__", - mx=mx.copy_modified(self_type=attribute_type), - ) - typ = map_instance_to_supertype(attribute_type, dunder_set.info) - dunder_set_type = expand_type_by_instance(bound_method, typ) - - callable_name = self.expr_checker.method_fullname(attribute_type, "__set__") - dunder_set_type = self.expr_checker.transform_callee_type( - callable_name, - dunder_set_type, - [TempNode(instance_type, context=context), rvalue], - [nodes.ARG_POS, nodes.ARG_POS], - context, - object_type=attribute_type, - ) - - # For non-overloaded setters, the result should be type-checked like a regular assignment. - # Hence, we first only try to infer the type by using the rvalue as type context. - type_context = rvalue - with self.msg.filter_errors(): - _, inferred_dunder_set_type = self.expr_checker.check_call( - dunder_set_type, - [TempNode(instance_type, context=context), type_context], - [nodes.ARG_POS, nodes.ARG_POS], - context, - object_type=attribute_type, - callable_name=callable_name, - ) - - # And now we in fact type check the call, to show errors related to wrong arguments - # count, etc., replacing the type context for non-overloaded setters only. - inferred_dunder_set_type = get_proper_type(inferred_dunder_set_type) - if isinstance(inferred_dunder_set_type, CallableType): - type_context = TempNode(AnyType(TypeOfAny.special_form), context=context) - self.expr_checker.check_call( - dunder_set_type, - [TempNode(instance_type, context=context), type_context], - [nodes.ARG_POS, nodes.ARG_POS], - context, - object_type=attribute_type, - callable_name=callable_name, - ) - - # Search for possible deprecations: - mx.chk.check_deprecated(dunder_set, mx.context) - mx.chk.warn_deprecated_overload_item( - dunder_set, mx.context, target=inferred_dunder_set_type, selftype=attribute_type - ) - - # In the following cases, a message already will have been recorded in check_call. - if (not isinstance(inferred_dunder_set_type, CallableType)) or ( - len(inferred_dunder_set_type.arg_types) < 2 - ): - return AnyType(TypeOfAny.from_error), get_type, False - - set_type = inferred_dunder_set_type.arg_types[1] -======= ->>>>>>> df9ddfcac (Consolidate descriptor handling in checkmember.py (#18831)) # Special case: if the rvalue_type is a subtype of both '__get__' and '__set__' types, # and '__get__' type is narrower than '__set__', then we invoke the binder to narrow type # by this assignment. Technically, this is not safe, but in practice this is # what a user expects. -<<<<<<< HEAD - rvalue_type = self.check_simple_assignment(set_type, rvalue, context) - infer = is_subtype(rvalue_type, get_type) and is_subtype(get_type, set_type) - return rvalue_type if infer else set_type, get_type, infer -======= - rvalue_type, _ = self.check_simple_assignment(attribute_type, rvalue, context) + rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) infer = is_subtype(rvalue_type, get_lvalue_type) and is_subtype( get_lvalue_type, attribute_type ) return rvalue_type if infer else attribute_type, attribute_type, infer ->>>>>>> df9ddfcac (Consolidate descriptor handling in checkmember.py (#18831)) def check_indexed_assignment( self, lvalue: IndexExpr, rvalue: Expression, context: Context From 8ed26e5eebb214f46b739b3f4bbd1ac6894450de Mon Sep 17 00:00:00 2001 From: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> Date: Mon, 31 Mar 2025 02:29:50 +0200 Subject: [PATCH 05/20] Fix crash on multiple unpacks in a bare type application (#18857) Fixes #18856. This should be done by `TypeAnalyzer.anal_array` but is not - semanal only invokes its own wrapper around `anal_type` --------- Co-authored-by: Ivan Levkivskyi --- mypy/semanal.py | 2 ++ mypy/typeanal.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index a3e113252ac3..66ef9f0abeef 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6041,6 +6041,8 @@ def analyze_type_application_args(self, expr: IndexExpr) -> list[Type] | None: return None types.append(analyzed) + if allow_unpack: + types = self.type_analyzer().check_unpacks_in_list(types) if has_param_spec and num_args == 1 and types: first_arg = get_proper_type(types[0]) single_any = len(types) == 1 and isinstance(first_arg, AnyType) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 9208630937e7..7bf21709b863 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -2006,7 +2006,7 @@ def check_unpacks_in_list(self, items: list[Type]) -> list[Type]: if num_unpacks > 1: assert final_unpack is not None - self.fail("More than one Unpack in a type is not allowed", final_unpack) + self.fail("More than one Unpack in a type is not allowed", final_unpack.type) return new_items def tuple_type(self, items: list[Type], line: int, column: int) -> TupleType: From a499d9fdba06732248c07586f2fd95c47a4fa0f7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 27 May 2025 16:10:06 +0100 Subject: [PATCH 06/20] Document --allow-redefinition-new (#19153) The feature was introduced in #18727. --- docs/source/command_line.rst | 50 ++++++++++++++++++++++++++++++++++-- docs/source/config_file.rst | 39 ++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index b455e287017e..dfed280d12ed 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -593,12 +593,58 @@ of the above sections. This flag causes mypy to suppress errors caused by not being able to fully infer the types of global and class variables. -.. option:: --allow-redefinition +.. option:: --allow-redefinition-new By default, mypy won't allow a variable to be redefined with an - unrelated type. This flag enables redefinition of a variable with an + unrelated type. This *experimental* flag enables the redefinition of + unannotated variables with an arbitrary type. You will also need to enable + :option:`--local-partial-types `. + Example: + + .. code-block:: python + + def maybe_convert(n: int, b: bool) -> int | str: + if b: + x = str(n) # Assign "str" + else: + x = n # Assign "int" + # Type of "x" is "int | str" here. + return x + + Without the new flag, mypy only supports inferring optional types + (``X | None``) from multiple assignments. With this option enabled, + mypy can infer arbitrary union types. + + This also enables an unannotated variable to have different types in different + code locations: + + .. code-block:: python + + if check(): + for x in range(n): + # Type of "x" is "int" here. + ... + else: + for x in ['a', 'b']: + # Type of "x" is "str" here. + ... + + Note: We are planning to turn this flag on by default in a future mypy + release, along with :option:`--local-partial-types `. + The feature is still experimental, and the semantics may still change. + +.. option:: --allow-redefinition + + This is an older variant of + :option:`--allow-redefinition-new `. + This flag enables redefinition of a variable with an arbitrary type *in some contexts*: only redefinitions within the same block and nesting depth as the original definition are allowed. + + We have no plans to remove this flag, but we expect that + :option:`--allow-redefinition-new ` + will replace this flag for new use cases eventually. + Example where this can be useful: .. code-block:: python diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index de51f0c796fd..9f23617b9481 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -713,6 +713,44 @@ section of the command line docs. Causes mypy to suppress errors caused by not being able to fully infer the types of global and class variables. +.. confval:: allow_redefinition_new + + :type: boolean + :default: False + + By default, mypy won't allow a variable to be redefined with an + unrelated type. This *experimental* flag enables the redefinition of + unannotated variables with an arbitrary type. You will also need to enable + :confval:`local_partial_types`. + Example: + + .. code-block:: python + + def maybe_convert(n: int, b: bool) -> int | str: + if b: + x = str(n) # Assign "str" + else: + x = n # Assign "int" + # Type of "x" is "int | str" here. + return x + + This also enables an unannotated variable to have different types in different + code locations: + + .. code-block:: python + + if check(): + for x in range(n): + # Type of "x" is "int" here. + ... + else: + for x in ['a', 'b']: + # Type of "x" is "str" here. + ... + + Note: We are planning to turn this flag on by default in a future mypy + release, along with :confval:`local_partial_types`. + .. confval:: allow_redefinition :type: boolean @@ -746,6 +784,7 @@ section of the command line docs. Disallows inferring variable type for ``None`` from two assignments in different scopes. This is always implicitly enabled when using the :ref:`mypy daemon `. + This will be enabled by default in a future mypy release. .. confval:: disable_error_code From 334469f999c5c777124a123062b4349614447e0d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 27 May 2025 16:59:29 +0100 Subject: [PATCH 07/20] [mypyc] Improve documentation of native and non-native classes (#19154) Also discuss `mypyc_attr(native_class=<...>)`. --- mypyc/doc/native_classes.rst | 82 ++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/mypyc/doc/native_classes.rst b/mypyc/doc/native_classes.rst index 7f892de3e239..dbcf238b78d5 100644 --- a/mypyc/doc/native_classes.rst +++ b/mypyc/doc/native_classes.rst @@ -48,11 +48,13 @@ can be assigned to (similar to using ``__slots__``):: Inheritance ----------- -Only single inheritance is supported (except for :ref:`traits -`). Most non-native classes can't be used as base -classes. +Only single inheritance is supported from native classes (except for +:ref:`traits `). Most non-native extension classes can't +be used as base classes, but regular Python classes can be used as +base classes unless they use unsupported metaclasses (see below for +more about this). -These non-native classes can be used as base classes of native +These non-native extension classes can be used as base classes of native classes: * ``object`` @@ -63,8 +65,6 @@ classes: * ``IndexError`` * ``LookupError`` * ``UserWarning`` -* ``typing.NamedTuple`` -* ``enum.Enum`` By default, a non-native class can't inherit a native class, and you can't inherit from a native class outside the compilation unit that @@ -89,6 +89,15 @@ You need to install ``mypy-extensions`` to use ``@mypyc_attr``: pip install --upgrade mypy-extensions +Additionally, mypyc recognizes these base classes as special, and +understands how they alter the behavior of classes (including native +classes) that subclass them: + +* ``typing.NamedTuple`` +* ``typing.Generic`` +* ``typing.Protocol`` +* ``enum.Enum`` + Class variables --------------- @@ -145,7 +154,8 @@ behavior is too dynamic. You can use these metaclasses, however: .. note:: If a class definition uses an unsupported metaclass, *mypyc - compiles the class into a regular Python class*. + compiles the class into a regular Python class* (non-native + class). Class decorators ---------------- @@ -165,7 +175,63 @@ efficient as pure native classes. .. note:: If a class definition uses an unsupported class decorator, *mypyc - compiles the class into a regular Python class*. + compiles the class into a regular Python class* (non-native class). + +Defining non-native classes +--------------------------- + +You can use the ``@mypy_extensions.mypyc_attr(...)`` class decorator +with an argument ``native_class=False`` to explicitly define normal +Python classes (non-native classes):: + + from mypy_extensions import mypyc_attr + + @mypyc_attr(native_class=False) + class NonNative: + def __init__(self) -> None: + self.attr = 1 + + setattr(NonNative, "extra", 1) # Ok + +This only has an effect in classes compiled using mypyc. Non-native +classes are significantly less efficient than native classes, but they +are sometimes necessary to work around the limitations of native classes. + +Non-native classes can use arbitrary metaclasses and class decorators, +and they support flexible multiple inheritance. Mypyc will still +generate a compile-time error if you try to assign to a method, or an +attribute that is not defined in a class body, since these are static +type errors detected by mypy:: + + o = NonNative() + o.extra = "x" # Static type error: "extra" not defined + +However, these operations still work at runtime, including in modules +that are not compiled using mypyc. You can also use ``setattr`` and +``getattr`` for dynamic access of arbitrary attributes. Expressions +with an ``Any`` type are also not type checked statically, allowing +access to arbitrary attributes:: + + a: Any = o + a.extra = "x" # Ok + + setattr(o, "extra", "y") # Also ok + +Implicit non-native classes +--------------------------- + +If a compiled class uses an unsupported metaclass or an unsupported +class decorator, it will implicitly be a non-native class, as +discussed above. You can still use ``@mypyc_attr(native_class=False)`` +to explicitly mark it as a non-native class. + +Explicit native classes +----------------------- + +You can use ``@mypyc_attr(native_class=True)`` to explicitly declare a +class as a native class. It will be a compile-time error if mypyc +can't compile the class as a native class. You can use this to avoid +accidentally defining implicit non-native classes. Deleting attributes ------------------- From b6da4fcf97fca9cd28e81a880f818dc364b5a06d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 28 May 2025 14:32:05 +0100 Subject: [PATCH 08/20] Allow enum members to have type objects as values (#19160) Type objects as enum values are supported at runtime. Fixes #19151. --- mypy/nodes.py | 4 ++-- test-data/unit/check-python310.test | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 584e56667944..2fb459142066 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3313,8 +3313,8 @@ def enum_members(self) -> list[str]: continue # unannotated value not a member typ = mypy.types.get_proper_type(sym.node.type) - if isinstance( - typ, mypy.types.FunctionLike + if ( + isinstance(typ, mypy.types.FunctionLike) and not typ.is_type_obj() ) or ( # explicit `@member` is required isinstance(typ, mypy.types.Instance) and typ.type.fullname == "enum.nonmember" diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index c2e2e5bddb34..af3982f6accd 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2638,3 +2638,24 @@ def f2() -> None: return reveal_type(y) # N: Revealed type is "builtins.str" [builtins fixtures/list.pyi] + +[case testEnumTypeObjectMember] +import enum +from typing import NoReturn + +def assert_never(x: NoReturn) -> None: ... + +class ValueType(enum.Enum): + INT = int + STR = str + +value_type: ValueType = ValueType.INT + +match value_type: + case ValueType.INT: + pass + case ValueType.STR: + pass + case _: + assert_never(value_type) +[builtins fixtures/tuple.pyi] From 2a036e739f8691576056669371d9f020e95c2603 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 29 May 2025 10:40:42 +0100 Subject: [PATCH 09/20] Revert "Infer correct types with overloads of `Type[Guard | Is]` (#19161) This reverts commit 43ea203e566901510dbdd59e8907fcddb2a8ee70 (#17678). The commit caused a regression (#19139). If we can't fix the regression soon enough, reverting the original change temporarily will at least unblock the mypy public release. The reverted PR can be merged again once the regression is fixed. --- mypy/checker.py | 24 +----- mypy/checkexpr.py | 83 +++---------------- test-data/unit/check-typeguard.test | 56 ------------- test-data/unit/check-typeis.test | 119 ---------------------------- 4 files changed, 14 insertions(+), 268 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index aceb0291926a..9c389cccd95f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6160,31 +6160,15 @@ def find_isinstance_check_helper( # considered "always right" (i.e. even if the types are not overlapping). # Also note that a care must be taken to unwrap this back at read places # where we use this to narrow down declared type. - with self.msg.filter_errors(), self.local_type_map(): - # `node.callee` can be an `overload`ed function, - # we need to resolve the real `overload` case. - _, real_func = self.expr_checker.check_call( - get_proper_type(self.lookup_type(node.callee)), - node.args, - node.arg_kinds, - node, - node.arg_names, - ) - real_func = get_proper_type(real_func) - if not isinstance(real_func, CallableType) or not ( - real_func.type_guard or real_func.type_is - ): - return {}, {} - - if real_func.type_guard is not None: - return {expr: TypeGuardedType(real_func.type_guard)}, {} + if node.callee.type_guard is not None: + return {expr: TypeGuardedType(node.callee.type_guard)}, {} else: - assert real_func.type_is is not None + assert node.callee.type_is is not None return conditional_types_to_typemaps( expr, *self.conditional_types_with_intersection( self.lookup_type(expr), - [TypeRange(real_func.type_is, is_upper_bound=False)], + [TypeRange(node.callee.type_is, is_upper_bound=False)], expr, ), ) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ba2d38b6f528..f375c14c5fc4 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2920,37 +2920,16 @@ def infer_overload_return_type( elif all_same_types([erase_type(typ) for typ in return_types]): self.chk.store_types(type_maps[0]) return erase_type(return_types[0]), erase_type(inferred_types[0]) - return self.check_call( - callee=AnyType(TypeOfAny.special_form), - args=args, - arg_kinds=arg_kinds, - arg_names=arg_names, - context=context, - callable_name=callable_name, - object_type=object_type, - ) - elif not all_same_type_narrowers(matches): - # This is an example of how overloads can be: - # - # @overload - # def is_int(obj: float) -> TypeGuard[float]: ... - # @overload - # def is_int(obj: int) -> TypeGuard[int]: ... - # - # x: Any - # if is_int(x): - # reveal_type(x) # N: int | float - # - # So, we need to check that special case. - return self.check_call( - callee=self.combine_function_signatures(cast("list[ProperType]", matches)), - args=args, - arg_kinds=arg_kinds, - arg_names=arg_names, - context=context, - callable_name=callable_name, - object_type=object_type, - ) + else: + return self.check_call( + callee=AnyType(TypeOfAny.special_form), + args=args, + arg_kinds=arg_kinds, + arg_names=arg_names, + context=context, + callable_name=callable_name, + object_type=object_type, + ) else: # Success! No ambiguity; return the first match. self.chk.store_types(type_maps[0]) @@ -3165,8 +3144,6 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call new_args: list[list[Type]] = [[] for _ in range(len(callables[0].arg_types))] new_kinds = list(callables[0].arg_kinds) new_returns: list[Type] = [] - new_type_guards: list[Type] = [] - new_type_narrowers: list[Type] = [] too_complex = False for target in callables: @@ -3193,25 +3170,8 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call for i, arg in enumerate(target.arg_types): new_args[i].append(arg) new_returns.append(target.ret_type) - if target.type_guard: - new_type_guards.append(target.type_guard) - if target.type_is: - new_type_narrowers.append(target.type_is) - - if new_type_guards and new_type_narrowers: - # They cannot be defined at the same time, - # declaring this function as too complex! - too_complex = True - union_type_guard = None - union_type_is = None - else: - union_type_guard = make_simplified_union(new_type_guards) if new_type_guards else None - union_type_is = ( - make_simplified_union(new_type_narrowers) if new_type_narrowers else None - ) union_return = make_simplified_union(new_returns) - if too_complex: any = AnyType(TypeOfAny.special_form) return callables[0].copy_modified( @@ -3221,8 +3181,6 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call ret_type=union_return, variables=variables, implicit=True, - type_guard=union_type_guard, - type_is=union_type_is, ) final_args = [] @@ -3236,8 +3194,6 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call ret_type=union_return, variables=variables, implicit=True, - type_guard=union_type_guard, - type_is=union_type_is, ) def erased_signature_similarity( @@ -6594,25 +6550,6 @@ def all_same_types(types: list[Type]) -> bool: return all(is_same_type(t, types[0]) for t in types[1:]) -def all_same_type_narrowers(types: list[CallableType]) -> bool: - if len(types) <= 1: - return True - - type_guards: list[Type] = [] - type_narrowers: list[Type] = [] - - for typ in types: - if typ.type_guard: - type_guards.append(typ.type_guard) - if typ.type_is: - type_narrowers.append(typ.type_is) - if type_guards and type_narrowers: - # Some overloads declare `TypeGuard` and some declare `TypeIs`, - # we cannot handle this in a union. - return False - return all_same_types(type_guards) and all_same_types(type_narrowers) - - def merge_typevars_in_callables_by_name( callables: Sequence[CallableType], ) -> tuple[list[CallableType], list[TypeVarType]]: diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index 00bf7d211927..94aa7ec6ffb8 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -731,62 +731,6 @@ assert a(x=x) reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[case testTypeGuardInOverloads] -from typing import Any, overload, Union -from typing_extensions import TypeGuard - -@overload -def func1(x: str) -> TypeGuard[str]: - ... - -@overload -def func1(x: int) -> TypeGuard[int]: - ... - -def func1(x: Any) -> Any: - return True - -def func2(val: Any): - if func1(val): - reveal_type(val) # N: Revealed type is "Union[builtins.str, builtins.int]" - else: - reveal_type(val) # N: Revealed type is "Any" - -def func3(val: Union[int, str]): - if func1(val): - reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]" - else: - reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]" - -def func4(val: int): - if func1(val): - reveal_type(val) # N: Revealed type is "builtins.int" - else: - reveal_type(val) # N: Revealed type is "builtins.int" -[builtins fixtures/tuple.pyi] - -[case testTypeIsInOverloadsSameReturn] -from typing import Any, overload, Union -from typing_extensions import TypeGuard - -@overload -def func1(x: str) -> TypeGuard[str]: - ... - -@overload -def func1(x: int) -> TypeGuard[str]: - ... - -def func1(x: Any) -> Any: - return True - -def func2(val: Union[int, str]): - if func1(val): - reveal_type(val) # N: Revealed type is "builtins.str" - else: - reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]" -[builtins fixtures/tuple.pyi] - [case testTypeGuardRestrictAwaySingleInvariant] from typing import List from typing_extensions import TypeGuard diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 8cdcf8634788..356b1abfdf63 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -818,125 +818,6 @@ accept_typeguard(typeguard) [builtins fixtures/tuple.pyi] -[case testTypeIsInOverloads] -from typing import Any, overload, Union -from typing_extensions import TypeIs - -@overload -def func1(x: str) -> TypeIs[str]: - ... - -@overload -def func1(x: int) -> TypeIs[int]: - ... - -def func1(x: Any) -> Any: - return True - -def func2(val: Any): - if func1(val): - reveal_type(val) # N: Revealed type is "Union[builtins.str, builtins.int]" - else: - reveal_type(val) # N: Revealed type is "Any" - -def func3(val: Union[int, str]): - if func1(val): - reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]" - else: - reveal_type(val) - -def func4(val: int): - if func1(val): - reveal_type(val) # N: Revealed type is "builtins.int" - else: - reveal_type(val) -[builtins fixtures/tuple.pyi] - -[case testTypeIsInOverloadsSameReturn] -from typing import Any, overload, Union -from typing_extensions import TypeIs - -@overload -def func1(x: str) -> TypeIs[str]: - ... - -@overload -def func1(x: int) -> TypeIs[str]: # type: ignore - ... - -def func1(x: Any) -> Any: - return True - -def func2(val: Union[int, str]): - if func1(val): - reveal_type(val) # N: Revealed type is "builtins.str" - else: - reveal_type(val) # N: Revealed type is "builtins.int" -[builtins fixtures/tuple.pyi] - -[case testTypeIsInOverloadsUnionizeError] -from typing import Any, overload, Union -from typing_extensions import TypeIs, TypeGuard - -@overload -def func1(x: str) -> TypeIs[str]: - ... - -@overload -def func1(x: int) -> TypeGuard[int]: - ... - -def func1(x: Any) -> Any: - return True - -def func2(val: Union[int, str]): - if func1(val): - reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]" - else: - reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]" -[builtins fixtures/tuple.pyi] - -[case testTypeIsInOverloadsUnionizeError2] -from typing import Any, overload, Union -from typing_extensions import TypeIs, TypeGuard - -@overload -def func1(x: int) -> TypeGuard[int]: - ... - -@overload -def func1(x: str) -> TypeIs[str]: - ... - -def func1(x: Any) -> Any: - return True - -def func2(val: Union[int, str]): - if func1(val): - reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]" - else: - reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]" -[builtins fixtures/tuple.pyi] - -[case testTypeIsLikeIsDataclass] -from typing import Any, overload, Union, Type -from typing_extensions import TypeIs - -class DataclassInstance: ... - -@overload -def is_dataclass(obj: type) -> TypeIs[Type[DataclassInstance]]: ... -@overload -def is_dataclass(obj: object) -> TypeIs[Union[DataclassInstance, Type[DataclassInstance]]]: ... - -def is_dataclass(obj: Union[type, object]) -> bool: - return False - -def func(arg: Any) -> None: - if is_dataclass(arg): - reveal_type(arg) # N: Revealed type is "Union[Type[__main__.DataclassInstance], __main__.DataclassInstance]" -[builtins fixtures/tuple.pyi] - [case testTypeIsEnumOverlappingUnionExcludesIrrelevant] from enum import Enum from typing import Literal From 8fe719ff3a8d1e26d3841a48df21ddf7d5b3c94d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 29 May 2025 12:36:32 +0100 Subject: [PATCH 10/20] Add changelog for 1.16 (#19138) Related to #18739. --- CHANGELOG.md | 415 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 405 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc87cae5065..01d58ce6a1b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,32 +2,173 @@ ## Next Release +## Mypy 1.16 + +We’ve just uploaded mypy 1.16 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). +Mypy is a static type checker for Python. This release includes new features and bug fixes. +You can install it as follows: + + python3 -m pip install -U mypy + +You can read the full documentation for this release on [Read the Docs](http://mypy.readthedocs.io). + ### Different Property Getter and Setter Types -Mypy now supports using different types for property getter and setter. +Mypy now supports using different types for a property getter and setter: + ```python class A: - value: int + _value: int @property - def f(self) -> int: - return self.value - @f.setter - def f(self, x: str | int) -> None: + def foo(self) -> int: + return self._value + + @foo.setter + def foo(self, x: str | int) -> None: try: - self.value = int(x) + self._value = int(x) except ValueError: - raise Exception(f"'{x}' is not a valid value for 'f'") + raise Exception(f"'{x}' is not a valid value for 'foo'") ``` +This was contributed by Ivan Levkivskyi (PR [18510](https://github.com/python/mypy/pull/18510)). + +### Flexible Variable Redefinitions (Experimental) + +Mypy now allows unannotated variables to be freely redefined with +different types when using the experimental `--allow-redefinition-new` +flag. You will also need to enable `--local-partial-types`. Mypy will +now infer a union type when different types are assigned to a +variable: -Contributed by Ivan Levkivskyi (PR [18510](https://github.com/python/mypy/pull/18510)) +```py +# mypy: allow-redefinition-new, local-partial-types + +def f(n: int, b: bool) -> int | str: + if b: + x = n + else: + x = str(n) + # Type of 'x' is int | str here. + return x +``` + +Without the new flag, mypy only supports inferring optional types (`X +| None`) from multiple assignments, but now mypy can infer arbitrary +union types. + +An unannotated variable can now also have different types in different +code locations: + +```py +# mypy: allow-redefinition-new, local-partial-types +... + +if cond(): + for x in range(n): + # Type of 'x' is 'int' here + ... +else: + for x in ['a', 'b']: + # Type of 'x' is 'str' here + ... +``` + +We are planning to turn this flag on by default in mypy 2.0, along +with `--local-partial-types`. The feature is still experimental and +has known issues, and the semantics may still change in the +future. You may need to update or add type annotations when switching +to the new behavior, but if you encounter anything unexpected, please +create a GitHub issue. + +This was contributed by Jukka Lehtosalo +(PR [18727](https://github.com/python/mypy/pull/18727), PR [19153](https://github.com/python/mypy/pull/19153)). + +### Stricter Type Checking with Imprecise Types + +Mypy can now detect additional errors in code that uses `Any` types or has missing function annotations. + +When calling `dict.get(x, None)` on an object of type `dict[str, Any]`, this +now results in an optional type (in the past it was `Any`): + +```python +def f(d: dict[str, Any]) -> int: + # Error: Return value has type "Any | None" but expected "int" + return d.get("x", None) +``` + +Type narrowing using assignments can result in more precise types in +the presence of `Any` types: + +```python +def foo(): ... + +def bar(n: int) -> None: + x = foo() + # Type of 'x' is 'Any' here + if n > 5: + x = str(n) + # Type of 'x' is 'str' here +``` + +When using `--check-untyped-defs`, unannotated overrides are now +checked more strictly against superclass definitions. + +Related PRs: + + * Use union types instead of join in binder (Ivan Levkivskyi, PR [18538](https://github.com/python/mypy/pull/18538)) + * Check superclass compatibility of untyped methods if `--check-untyped-defs` is set (Stanislav Terliakov, PR [18970](https://github.com/python/mypy/pull/18970)) + +### Improvements to Attribute Resolution + +This release includes several fixes to inconsistent resolution of attribute, method and descriptor types. + + * Consolidate descriptor handling (Ivan Levkivskyi, PR [18831](https://github.com/python/mypy/pull/18831)) + * Make multiple inheritance checking use common semantics (Ivan Levkivskyi, PR [18876](https://github.com/python/mypy/pull/18876)) + * Make method override checking use common semantics (Ivan Levkivskyi, PR [18870](https://github.com/python/mypy/pull/18870)) + * Fix descriptor overload selection (Ivan Levkivskyi, PR [18868](https://github.com/python/mypy/pull/18868)) + * Handle union types when binding `self` (Ivan Levkivskyi, PR [18867](https://github.com/python/mypy/pull/18867)) + * Make variable override checking use common semantics (Ivan Levkivskyi, PR [18847](https://github.com/python/mypy/pull/18847)) + * Make descriptor handling behave consistently (Ivan Levkivskyi, PR [18831](https://github.com/python/mypy/pull/18831)) + +### Make Implementation for Abstract Overloads Optional + +The implementation can now be omitted for abstract overloaded methods, +even outside stubs: + +```py +from abc import abstractmethod +from typing import overload + +class C: + @abstractmethod + @overload + def foo(self, x: int) -> int: ... + + @abstractmethod + @overload + def foo(self, x: str) -> str: ... + + # No implementation required for "foo" +``` + +This was contributed by Ivan Levkivskyi (PR [18882](https://github.com/python/mypy/pull/18882)). + +### Option to Exclude Everything in .gitignore + +You can now use `--exclude-gitignore` to exclude everything in a +`.gitignore` file from the mypy build. This behaves similar to +excluding the paths using `--exclude`. We might enable this by default +in a future mypy release. + +This was contributed by Ivan Levkivskyi (PR [18696](https://github.com/python/mypy/pull/18696)). ### Selectively Disable Deprecated Warnings It's now possible to selectively disable warnings generated from [`warnings.deprecated`](https://docs.python.org/3/library/warnings.html#warnings.deprecated) using the [`--deprecated-calls-exclude`](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-deprecated-calls-exclude) -option. +option: ```python # mypy --enable-error-code deprecated @@ -35,16 +176,269 @@ option. import foo foo.A().func() # OK, the deprecated warning is ignored +``` +```python # file foo.py + from typing_extensions import deprecated + class A: @deprecated("Use A.func2 instead") def func(self): pass + + ... ``` Contributed by Marc Mueller (PR [18641](https://github.com/python/mypy/pull/18641)) +### Annotating Native/Non-Native Classes in Mypyc + +You can now declare a class as a non-native class when compiling with +mypyc. Unlike native classes, which are extension classes and have an +immutable structure, non-native classes are normal Python classes at +runtime and are fully dynamic. Example: + +```python +from mypy_extensions import mypyc_attr + +@mypyc_attr(native_class=False) +class NonNativeClass: + ... + +o = NonNativeClass() + +# Ok, even if attribute "foo" not declared in class body +setattr(o, "foo", 1) +``` + +Classes are native by default in compiled modules, but classes that +use certain features (such as most metaclasses) are implicitly +non-native. + +You can also explicitly declare a class as native. In this case mypyc +will generate an error if it can't compile the class as a native +class, instead of falling back to a non-native class: + +```python +from mypy_extensions import mypyc_attr +from foo import MyMeta + +# Error: Unsupported metaclass for a native class +@mypyc_attr(native_class=True) +class C(metaclass=MyMeta): + ... +``` + +Since native classes are significantly more efficient that non-native +classes, you may want to ensure that certain classes always compiled +as native classes. + +This feature was contributed by Valentin Stanciu (PR [18802](https://github.com/python/mypy/pull/18802)). + +### Mypyc Fixes and Improvements + + * Improve documentation of native and non-native classes (Jukka Lehtosalo, PR [19154](https://github.com/python/mypy/pull/19154)) + * Fix compilation when using Python 3.13 debug build (Valentin Stanciu, PR [19045](https://github.com/python/mypy/pull/19045)) + * Show the reason why a class can't be a native class (Valentin Stanciu, PR [19016](https://github.com/python/mypy/pull/19016)) + * Support await/yield while temporary values are live (Michael J. Sullivan, PR [16305](https://github.com/python/mypy/pull/16305)) + * Fix spilling values with overlapping error values (Jukka Lehtosalo, PR [18961](https://github.com/python/mypy/pull/18961)) + * Fix reference count of spilled register in async def (Jukka Lehtosalo, PR [18957](https://github.com/python/mypy/pull/18957)) + * Add basic optimization for `sorted` (Marc Mueller, PR [18902](https://github.com/python/mypy/pull/18902)) + * Fix access of class object in a type annotation (Advait Dixit, PR [18874](https://github.com/python/mypy/pull/18874)) + * Optimize `list.__imul__` and `tuple.__mul__ `(Marc Mueller, PR [18887](https://github.com/python/mypy/pull/18887)) + * Optimize `list.__add__`, `list.__iadd__` and `tuple.__add__` (Marc Mueller, PR [18845](https://github.com/python/mypy/pull/18845)) + * Add and implement primitive `list.copy()` (exertustfm, PR [18771](https://github.com/python/mypy/pull/18771)) + * Optimize `builtins.repr` (Marc Mueller, PR [18844](https://github.com/python/mypy/pull/18844)) + * Support iterating over keys/values/items of dict-bound TypeVar and ParamSpec.kwargs (Stanislav Terliakov, PR [18789](https://github.com/python/mypy/pull/18789)) + * Add efficient primitives for `str.strip()` etc. (Advait Dixit, PR [18742](https://github.com/python/mypy/pull/18742)) + * Document that `strip()` etc. are optimized (Jukka Lehtosalo, PR [18793](https://github.com/python/mypy/pull/18793)) + * Fix mypyc crash with enum type aliases (Valentin Stanciu, PR [18725](https://github.com/python/mypy/pull/18725)) + * Optimize `str.find` and `str.rfind` (Marc Mueller, PR [18709](https://github.com/python/mypy/pull/18709)) + * Optimize `str.__contains__` (Marc Mueller, PR [18705](https://github.com/python/mypy/pull/18705)) + * Fix order of steal/unborrow in tuple unpacking (Ivan Levkivskyi, PR [18732](https://github.com/python/mypy/pull/18732)) + * Optimize `str.partition` and `str.rpartition` (Marc Mueller, PR [18702](https://github.com/python/mypy/pull/18702)) + * Optimize `str.startswith` and `str.endswith` with tuple argument (Marc Mueller, PR [18678](https://github.com/python/mypy/pull/18678)) + * Improve `str.startswith` and `str.endswith` with tuple argument (Marc Mueller, PR [18703](https://github.com/python/mypy/pull/18703)) + * `pythoncapi_compat`: don't define Py_NULL if it is already defined (Michael R. Crusoe, PR [18699](https://github.com/python/mypy/pull/18699)) + * Optimize `str.splitlines` (Marc Mueller, PR [18677](https://github.com/python/mypy/pull/18677)) + * Mark `dict.setdefault` as optimized (Marc Mueller, PR [18685](https://github.com/python/mypy/pull/18685)) + * Support `__del__` methods (Advait Dixit, PR [18519](https://github.com/python/mypy/pull/18519)) + * Optimize `str.rsplit` (Marc Mueller, PR [18673](https://github.com/python/mypy/pull/18673)) + * Optimize `str.removeprefix` and `str.removesuffix` (Marc Mueller, PR [18672](https://github.com/python/mypy/pull/18672)) + * Recognize literal types in `__match_args__` (Stanislav Terliakov, PR [18636](https://github.com/python/mypy/pull/18636)) + * Fix non extension classes with attribute annotations using forward references (Valentin Stanciu, PR [18577](https://github.com/python/mypy/pull/18577)) + * Use lower-case generic types such as `list[t]` in documentation (Jukka Lehtosalo, PR [18576](https://github.com/python/mypy/pull/18576)) + * Improve support for `frozenset` (Marc Mueller, PR [18571](https://github.com/python/mypy/pull/18571)) + * Fix wheel build for cp313-win (Marc Mueller, PR [18560](https://github.com/python/mypy/pull/18560)) + * Reduce impact of immortality (introduced in Python 3.12) on reference counting performance (Jukka Lehtosalo, PR [18459](https://github.com/python/mypy/pull/18459)) + * Update math error messages for 3.14 (Marc Mueller, PR [18534](https://github.com/python/mypy/pull/18534)) + * Update math error messages for 3.14 (2) (Marc Mueller, PR [18949](https://github.com/python/mypy/pull/18949)) + * Replace deprecated `_PyLong_new` with `PyLongWriter` API (Marc Mueller, PR [18532](https://github.com/python/mypy/pull/18532)) + +### Fixes to Crashes + + * Traverse module ancestors when traversing reachable graph nodes during dmypy update (Stanislav Terliakov, PR [18906](https://github.com/python/mypy/pull/18906)) + * Fix crash on multiple unpacks in a bare type application (Stanislav Terliakov, PR [18857](https://github.com/python/mypy/pull/18857)) + * Prevent crash when enum/TypedDict call is stored as a class attribute (Stanislav Terliakov, PR [18861](https://github.com/python/mypy/pull/18861)) + * Fix crash on multiple unpacks in a bare type application (Stanislav Terliakov, PR [18857](https://github.com/python/mypy/pull/18857)) + * Fix crash on type inference against non-normal callables (Ivan Levkivskyi, PR [18858](https://github.com/python/mypy/pull/18858)) + * Fix crash on decorated getter in settable property (Ivan Levkivskyi, PR [18787](https://github.com/python/mypy/pull/18787)) + * Fix crash on callable with `*args` and suffix against Any (Ivan Levkivskyi, PR [18781](https://github.com/python/mypy/pull/18781)) + * Fix crash on deferred supertype and setter override (Ivan Levkivskyi, PR [18649](https://github.com/python/mypy/pull/18649)) + * Fix crashes on incorrectly detected recursive aliases (Ivan Levkivskyi, PR [18625](https://github.com/python/mypy/pull/18625)) + * Report that `NamedTuple` and `dataclass` are incompatile instead of crashing (Christoph Tyralla, PR [18633](https://github.com/python/mypy/pull/18633)) + * Fix mypy daemon crash (Valentin Stanciu, PR [19087](https://github.com/python/mypy/pull/19087)) + +### Performance Improvements + +These are specific to mypy. Mypyc-related performance improvements are discussed elsewhere. + + * Speed up binding `self` in trivial cases (Ivan Levkivskyi, PR [19024](https://github.com/python/mypy/pull/19024)) + * Small constraint solver optimization (Aaron Gokaslan, PR [18688](https://github.com/python/mypy/pull/18688)) + +### Documentation Updates + + * Improve documentation of `--strict` (lenayoung8, PR [18903](https://github.com/python/mypy/pull/18903)) + * Remove a note about `from __future__ import annotations` (Ageev Maxim, PR [18915](https://github.com/python/mypy/pull/18915)) + * Improve documentation on type narrowing (Tim Hoffmann, PR [18767](https://github.com/python/mypy/pull/18767)) + * Fix metaclass usage example (Georg, PR [18686](https://github.com/python/mypy/pull/18686)) + * Update documentation on `extra_checks` flag (Ivan Levkivskyi, PR [18537](https://github.com/python/mypy/pull/18537)) + +### Stubgen Improvements + + * Fix `TypeAlias` handling (Alexey Makridenko, PR [18960](https://github.com/python/mypy/pull/18960)) + * Handle `arg=None` in C extension modules (Anthony Sottile, PR [18768](https://github.com/python/mypy/pull/18768)) + * Fix valid type detection to allow pipe unions (Chad Dombrova, PR [18726](https://github.com/python/mypy/pull/18726)) + * Include simple decorators in stub files (Marc Mueller, PR [18489](https://github.com/python/mypy/pull/18489)) + * Support positional and keyword-only arguments in stubdoc (Paul Ganssle, PR [18762](https://github.com/python/mypy/pull/18762)) + * Fall back to `Incomplete` if we are unable to determine the module name (Stanislav Terliakov, PR [19084](https://github.com/python/mypy/pull/19084)) + +### Stubtest Improvements + + * Make stubtest ignore `__slotnames__` (Nick Pope, PR [19077](https://github.com/python/mypy/pull/19077)) + * Fix stubtest tests on 3.14 (Jelle Zijlstra, PR [19074](https://github.com/python/mypy/pull/19074)) + * Support for `strict_bytes` in stubtest (Joren Hammudoglu, PR [19002](https://github.com/python/mypy/pull/19002)) + * Understand override (Shantanu, PR [18815](https://github.com/python/mypy/pull/18815)) + * Better checking of runtime arguments with dunder names (Shantanu, PR [18756](https://github.com/python/mypy/pull/18756)) + * Ignore setattr and delattr inherited from object (Stephen Morton, PR [18325](https://github.com/python/mypy/pull/18325)) + +### Miscellaneous Fixes and Improvements + + * Add `--strict-bytes` to `--strict` (wyattscarpenter, PR [19049](https://github.com/python/mypy/pull/19049)) + * Admit that Final variables are never redefined (Stanislav Terliakov, PR [19083](https://github.com/python/mypy/pull/19083)) + * Add special support for `@django.cached_property` needed in `django-stubs` (sobolevn, PR [18959](https://github.com/python/mypy/pull/18959)) + * Do not narrow types to `Never` with binder (Ivan Levkivskyi, PR [18972](https://github.com/python/mypy/pull/18972)) + * Local forward references should precede global forward references (Ivan Levkivskyi, PR [19000](https://github.com/python/mypy/pull/19000)) + * Do not cache module lookup results in incremental mode that may become invalid (Stanislav Terliakov, PR [19044](https://github.com/python/mypy/pull/19044)) + * Only consider meta variables in ambiguous "any of" constraints (Stanislav Terliakov, PR [18986](https://github.com/python/mypy/pull/18986)) + * Allow accessing `__init__` on final classes and when `__init__` is final (Stanislav Terliakov, PR [19035](https://github.com/python/mypy/pull/19035)) + * Treat varargs as positional-only (A5rocks, PR [19022](https://github.com/python/mypy/pull/19022)) + * Enable colored output for argparse help in Python 3.14 (Marc Mueller, PR [19021](https://github.com/python/mypy/pull/19021)) + * Fix argparse for Python 3.14 (Marc Mueller, PR [19020](https://github.com/python/mypy/pull/19020)) + * `dmypy suggest` can now suggest through contextmanager-based decorators (Anthony Sottile, PR [18948](https://github.com/python/mypy/pull/18948)) + * Fix `__r__` being used under the same `____` hook (Arnav Jain, PR [18995](https://github.com/python/mypy/pull/18995)) + * Prioritize `.pyi` from `-stubs` packages over bundled `.pyi` (Joren Hammudoglu, PR [19001](https://github.com/python/mypy/pull/19001)) + * Fix missing subtype check case for `type[T]` (Stanislav Terliakov, PR [18975](https://github.com/python/mypy/pull/18975)) + * Fixes to the detection of redundant casts (Anthony Sottile, PR [18588](https://github.com/python/mypy/pull/18588)) + * Make some parse errors non-blocking (Shantanu, PR [18941](https://github.com/python/mypy/pull/18941)) + * Fix PEP 695 type alias with a mix of type arguments (PEP 696) (Marc Mueller, PR [18919](https://github.com/python/mypy/pull/18919)) + * Allow deeper recursion in mypy daemon, better error reporting (Carter Dodd, PR [17707](https://github.com/python/mypy/pull/17707)) + * Fix swapped errors for frozen/non-frozen dataclass inheritance (Nazrawi Demeke, PR [18918](https://github.com/python/mypy/pull/18918)) + * Fix incremental issue with namespace packages (Shantanu, PR [18907](https://github.com/python/mypy/pull/18907)) + * Exclude irrelevant members when narrowing union overlapping with enum (Stanislav Terliakov, PR [18897](https://github.com/python/mypy/pull/18897)) + * Flatten union before contracting literals when checking subtyping (Stanislav Terliakov, PR [18898](https://github.com/python/mypy/pull/18898)) + * Do not add `kw_only` dataclass fields to `__match_args__` (sobolevn, PR [18892](https://github.com/python/mypy/pull/18892)) + * Fix error message when returning long tuple with type mismatch (Thomas Mattone, PR [18881](https://github.com/python/mypy/pull/18881)) + * Treat `TypedDict` (old-style) aliases as regular `TypedDict`s (Stanislav Terliakov, PR [18852](https://github.com/python/mypy/pull/18852)) + * Warn about unused `type: ignore` comments when error code is disabled (Brian Schubert, PR [18849](https://github.com/python/mypy/pull/18849)) + * Reject duplicate `ParamSpec.{args,kwargs}` at call site (Stanislav Terliakov, PR [18854](https://github.com/python/mypy/pull/18854)) + * Make detection of enum members more consistent (sobolevn, PR [18675](https://github.com/python/mypy/pull/18675)) + * Admit that `**kwargs` mapping subtypes may have no direct type parameters (Stanislav Terliakov, PR [18850](https://github.com/python/mypy/pull/18850)) + * Don't suggest `types-setuptools` for `pkg_resources` (Shantanu, PR [18840](https://github.com/python/mypy/pull/18840)) + * Suggest `scipy-stubs` for `scipy` as non-typeshed stub package (Joren Hammudoglu, PR [18832](https://github.com/python/mypy/pull/18832)) + * Narrow tagged unions in match statements (Gene Parmesan Thomas, PR [18791](https://github.com/python/mypy/pull/18791)) + * Consistently store settable property type (Ivan Levkivskyi, PR [18774](https://github.com/python/mypy/pull/18774)) + * Do not blindly undefer on leaving function (Ivan Levkivskyi, PR [18674](https://github.com/python/mypy/pull/18674)) + * Process superclass methods before subclass methods in semanal (Ivan Levkivskyi, PR [18723](https://github.com/python/mypy/pull/18723)) + * Only defer top-level functions (Ivan Levkivskyi, PR [18718](https://github.com/python/mypy/pull/18718)) + * Add one more type-checking pass (Ivan Levkivskyi, PR [18717](https://github.com/python/mypy/pull/18717)) + * Properly account for `member` and `nonmember` in enums (sobolevn, PR [18559](https://github.com/python/mypy/pull/18559)) + * Fix instance vs tuple subtyping edge case (Ivan Levkivskyi, PR [18664](https://github.com/python/mypy/pull/18664)) + * Improve handling of Any/object in variadic generics (Ivan Levkivskyi, PR [18643](https://github.com/python/mypy/pull/18643)) + * Fix handling of named tuples in class match pattern (Ivan Levkivskyi, PR [18663](https://github.com/python/mypy/pull/18663)) + * Fix regression for user config files (Shantanu, PR [18656](https://github.com/python/mypy/pull/18656)) + * Fix dmypy socket issue on GNU/Hurd (Mattias Ellert, PR [18630](https://github.com/python/mypy/pull/18630)) + * Don't assume that for loop body index variable is always set (Jukka Lehtosalo, PR [18631](https://github.com/python/mypy/pull/18631)) + * Fix overlap check for variadic generics (Ivan Levkivskyi, PR [18638](https://github.com/python/mypy/pull/18638)) + * Improve support for `functools.partial` of overloaded callable protocol (Shantanu, PR [18639](https://github.com/python/mypy/pull/18639)) + * Allow lambdas in `except*` clauses (Stanislav Terliakov, PR [18620](https://github.com/python/mypy/pull/18620)) + * Fix trailing commas in many multiline string options in `pyproject.toml` (sobolevn, PR [18624](https://github.com/python/mypy/pull/18624)) + * Allow trailing commas for `files` setting in `mypy.ini` and `setup.ini` (sobolevn, PR [18621](https://github.com/python/mypy/pull/18621)) + * Fix "not callable" issue for `@dataclass(frozen=True)` with `Final` attr (sobolevn, PR [18572](https://github.com/python/mypy/pull/18572)) + * Add missing TypedDict special case when checking member access (Stanislav Terliakov, PR [18604](https://github.com/python/mypy/pull/18604)) + * Use lower case `list` and `dict` in invariance notes (Jukka Lehtosalo, PR [18594](https://github.com/python/mypy/pull/18594)) + * Fix inference when class and instance match protocol (Ivan Levkivskyi, PR [18587](https://github.com/python/mypy/pull/18587)) + * Remove support for `builtins.Any` (Marc Mueller, PR [18578](https://github.com/python/mypy/pull/18578)) + * Update the overlapping check for tuples to account for NamedTuples (A5rocks, PR [18564](https://github.com/python/mypy/pull/18564)) + * Fix `@deprecated` (PEP 702) with normal overloaded methods (Christoph Tyralla, PR [18477](https://github.com/python/mypy/pull/18477)) + * Start propagating end columns/lines for `type-arg` errors (A5rocks, PR [18533](https://github.com/python/mypy/pull/18533)) + * Improve handling of `type(x) is Foo` checks (Stanislav Terliakov, PR [18486](https://github.com/python/mypy/pull/18486)) + * Suggest `typing.Literal` for exit-return error messages (Marc Mueller, PR [18541](https://github.com/python/mypy/pull/18541)) + * Allow redefinitions in except/else/finally (Stanislav Terliakov, PR [18515](https://github.com/python/mypy/pull/18515)) + * Disallow setting Python version using inline config (Shantanu, PR [18497](https://github.com/python/mypy/pull/18497)) + * Improve type inference in tuple multiplication plugin (Shantanu, PR [18521](https://github.com/python/mypy/pull/18521)) + * Add missing line number to `yield from` with wrong type (Stanislav Terliakov, PR [18518](https://github.com/python/mypy/pull/18518)) + * Hint at argument names when formatting callables with compatible return types in error messages (Stanislav Terliakov, PR [18495](https://github.com/python/mypy/pull/18495)) + * Add better naming and improve compatibility for ad hoc intersections of instances (Christoph Tyralla, PR [18506](https://github.com/python/mypy/pull/18506)) + +### Acknowledgements + +Thanks to all mypy contributors who contributed to this release: + +- A5rocks +- Aaron Gokaslan +- Advait Dixit +- Ageev Maxim +- Alexey Makridenko +- Ali Hamdan +- Anthony Sottile +- Arnav Jain +- Brian Schubert +- bzoracler +- Carter Dodd +- Chad Dombrova +- Christoph Tyralla +- Dimitri Papadopoulos Orfanos +- Emma Smith +- exertustfm +- Gene Parmesan Thomas +- Georg +- Ivan Levkivskyi +- Jared Hance +- Jelle Zijlstra +- Joren Hammudoglu +- lenayoung8 +- Marc Mueller +- Mattias Ellert +- Michael J. Sullivan +- Michael R. Crusoe +- Nazrawi Demeke +- Nick Pope +- Paul Ganssle +- Shantanu +- sobolevn +- Stanislav Terliakov +- Stephen Morton +- Thomas Mattone +- Tim Hoffmann +- Tim Ruffing +- Valentin Stanciu +- Wesley Collin Wright +- wyattscarpenter + +I’d also like to thank my employer, Dropbox, for supporting mypy development. + ## Mypy 1.15 We’ve just uploaded mypy 1.15 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). @@ -408,6 +802,7 @@ This was contributed by Marc Mueller (PR [18014](https://github.com/python/mypy/ ### Other Notables Fixes and Improvements + * Allow enum members to have type objects as values (Jukka Lehtosalo, PR [19160](https://github.com/python/mypy/pull/19160)) * Show `Protocol` `__call__` for arguments with incompatible types (MechanicalConstruct, PR [18214](https://github.com/python/mypy/pull/18214)) * Make join and meet symmetric with `strict_optional` (MechanicalConstruct, PR [18227](https://github.com/python/mypy/pull/18227)) * Preserve block unreachablility when checking function definitions with constrained TypeVars (Brian Schubert, PR [18217](https://github.com/python/mypy/pull/18217)) From 9e72e9601f4c2fb6866cfec98fc40a31c91ccdb0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 29 May 2025 12:38:56 +0100 Subject: [PATCH 11/20] Update version to 1.16.0 --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index ffebfb7aa9ad..34fb2f642c5e 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.16.0+dev" +__version__ = "1.16.0" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) From 9b079f6592740a51c0e629728eeb0324ad85126f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 Jun 2025 13:25:18 +0100 Subject: [PATCH 12/20] Bump version to 1.16.1+dev --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index 34fb2f642c5e..d2d42a386dec 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.16.0" +__version__ = "1.16.1+dev" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) From 0a4f28431faa18e59d35bc269cb0ea6c00810653 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 3 Jun 2025 08:18:05 +0100 Subject: [PATCH 13/20] Fix crash on invalid property inside its own body (#19208) Fixes https://github.com/python/mypy/issues/19205 --- mypy/checkmember.py | 4 ++++ test-data/unit/check-classes.test | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index cc104fed0752..4821c622cb0c 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -974,6 +974,10 @@ def expand_and_bind_callable( assert isinstance(expanded, CallableType) if var.is_settable_property and mx.is_lvalue and var.setter_type is not None: # TODO: use check_call() to infer better type, same as for __set__(). + if not expanded.arg_types: + # This can happen when accessing invalid property from its own body, + # error will be reported elsewhere. + return AnyType(TypeOfAny.from_error) return expanded.arg_types[0] else: return expanded.ret_type diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index e0ea00aee361..59204cc31c43 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8726,3 +8726,16 @@ class Fields: reveal_type(Fields.bool_f) # N: Revealed type is "__main__.BoolField" reveal_type(Fields.int_f) # N: Revealed type is "__main__.NumField" reveal_type(Fields.custom_f) # N: Revealed type is "__main__.AnyField[__main__.Custom]" + +[case testRecursivePropertyWithInvalidSetterNoCrash] +class NoopPowerResource: + _hardware_type: int + + @property + def hardware_type(self) -> int: + return self._hardware_type + + @hardware_type.setter + def hardware_type(self) -> None: # E: Invalid property setter signature + self.hardware_type = None # Note: intentionally recursive +[builtins fixtures/property.pyi] From c39f5e73c47182e51c5d8d488f7cc7301257c974 Mon Sep 17 00:00:00 2001 From: Advait Dixit <48302999+advait-dixit@users.noreply.github.com> Date: Mon, 2 Jun 2025 07:20:25 -0700 Subject: [PATCH 14/20] [mypyc] Fixing condition for handling user-defined __del__ (#19188) Fixes #19175. Conditions for generating and invoking `del` method were not consistent. As things currently stand, this pull request fixes the crash. However, for classes that derive from Python built-ins, user-defined `__del__` will not be invoked. --- mypyc/codegen/emitclass.py | 6 +++--- mypyc/test-data/run-classes.test | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index c5191e5fb939..9cb9074b9fc4 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -304,9 +304,6 @@ def emit_line() -> None: emit_line() generate_dealloc_for_class(cl, dealloc_name, clear_name, bool(del_method), emitter) emit_line() - if del_method: - generate_finalize_for_class(del_method, finalize_name, emitter) - emit_line() if cl.allow_interpreted_subclasses: shadow_vtable_name: str | None = generate_vtables( @@ -317,6 +314,9 @@ def emit_line() -> None: shadow_vtable_name = None vtable_name = generate_vtables(cl, vtable_setup_name, vtable_name, emitter, shadow=False) emit_line() + if del_method: + generate_finalize_for_class(del_method, finalize_name, emitter) + emit_line() if needs_getseters: generate_getseter_declarations(cl, emitter) emit_line() diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 97bc063dd8ea..288f281c0a94 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2754,6 +2754,21 @@ def test_function(): assert(isinstance(d.fitem, ForwardDefinedClass)) assert(isinstance(d.fitems, ForwardDefinedClass)) +[case testDelForDictSubclass-xfail] +# The crash in issue mypy#19175 is fixed. +# But, for classes that derive from built-in Python classes, user-defined __del__ method is not +# being invoked. +class DictSubclass(dict): + def __del__(self): + print("deleting DictSubclass...") + +[file driver.py] +import native +native.DictSubclass() + +[out] +deleting DictSubclass... + [case testDel] class A: def __del__(self): @@ -2774,6 +2789,12 @@ class C(B): class D(A): pass +# Just make sure that this class compiles (see issue mypy#19175). testDelForDictSubclass tests for +# correct output. +class NormDict(dict): + def __del__(self) -> None: + pass + [file driver.py] import native native.C() From cb3c6ec6a7aaa96a0e26768a946ac63ea14115f2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 3 Jun 2025 18:25:02 +0100 Subject: [PATCH 15/20] Fix crash on partial type used as context (#19216) Fixes https://github.com/python/mypy/issues/19213 --- mypy/checker.py | 4 +++- test-data/unit/check-inference.test | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9c389cccd95f..cb465b390aa6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3426,7 +3426,9 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, rvalue: Expression) -> # store the rvalue type on the variable. actual_lvalue_type = None if lvalue_node.is_inferred and not lvalue_node.explicit_self_type: - rvalue_type = self.expr_checker.accept(rvalue, lvalue_node.type) + # Don't use partial types as context, similar to regular code path. + ctx = lvalue_node.type if not isinstance(lvalue_node.type, PartialType) else None + rvalue_type = self.expr_checker.accept(rvalue, ctx) actual_lvalue_type = lvalue_node.type lvalue_node.type = rvalue_type lvalue_type, _ = self.node_type_from_base(lvalue_node.name, lvalue_node.info, lvalue) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 25565946158e..093924ab2e26 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3979,3 +3979,24 @@ def check(mapping: Mapping[str, _T]) -> None: reveal_type(ok1) # N: Revealed type is "Union[_T`-1, builtins.str]" ok2: Union[_T, str] = mapping.get("", "") [builtins fixtures/tuple.pyi] + +[case testNoCrashOnPartialTypeAsContext] +from typing import overload, TypeVar, Optional, Protocol + +T = TypeVar("T") +class DbManager(Protocol): + @overload + def get(self, key: str) -> Optional[T]: + pass + + @overload + def get(self, key: str, default: T) -> T: + pass + +class Foo: + def __init__(self, db: DbManager, bar: bool) -> None: + if bar: + self.qux = db.get("qux") + else: + self.qux = {} # E: Need type annotation for "qux" (hint: "qux: Dict[, ] = ...") +[builtins fixtures/dict.pyi] From c86480ce51e4bb6db21f4b3f0b3ec8833aafc8ce Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 1 Jun 2025 02:30:37 +0100 Subject: [PATCH 16/20] Tighten metaclass __call__ handling in protocols (#19191) Fixes https://github.com/python/mypy/issues/19184 This fixes an (edge-case) regression introduced in 1.16. Fix is straightforward, only ignore `__call__` if it comes from an _actual_ metaclass. --- mypy/constraints.py | 4 ++-- mypy/nodes.py | 4 ++-- mypy/typeops.py | 2 +- test-data/unit/check-protocols.test | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 8e7a30e05ffb..73b7ec7954a4 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1066,8 +1066,8 @@ def infer_constraints_from_protocol_members( inst, erase_typevars(temp), ignore_pos_arg_names=True ): continue - # This exception matches the one in subtypes.py, see PR #14121 for context. - if member == "__call__" and instance.type.is_metaclass(): + # This exception matches the one in typeops.py, see PR #14121 for context. + if member == "__call__" and instance.type.is_metaclass(precise=True): continue res.extend(infer_constraints(temp, inst, self.direction)) if mypy.subtypes.IS_SETTABLE in mypy.subtypes.get_member_flags(member, protocol): diff --git a/mypy/nodes.py b/mypy/nodes.py index 2fb459142066..9a9403722a16 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3371,11 +3371,11 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None: return c return None - def is_metaclass(self) -> bool: + def is_metaclass(self, *, precise: bool = False) -> bool: return ( self.has_base("builtins.type") or self.fullname == "abc.ABCMeta" - or self.fallback_to_any + or (self.fallback_to_any and not precise) ) def has_base(self, fullname: str) -> bool: diff --git a/mypy/typeops.py b/mypy/typeops.py index bcf946900563..3715081ae173 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1257,7 +1257,7 @@ def named_type(fullname: str) -> Instance: return type_object_type(left.type, named_type) - if member == "__call__" and left.type.is_metaclass(): + if member == "__call__" and left.type.is_metaclass(precise=True): # Special case: we want to avoid falling back to metaclass __call__ # if constructor signature didn't match, this can cause many false negatives. return None diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 34e3f3e88080..56c3d72d23e2 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4460,3 +4460,25 @@ f2(a4) # E: Argument 1 to "f2" has incompatible type "A4"; expected "P2" \ # N: foo: expected "B1", got "str" \ # N: foo: expected setter type "C1", got "str" [builtins fixtures/property.pyi] + +[case testInferCallableProtoWithAnySubclass] +from typing import Any, Generic, Protocol, TypeVar + +T = TypeVar("T", covariant=True) + +Unknown: Any +class Mock(Unknown): + def __init__(self, **kwargs: Any) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + +class Factory(Protocol[T]): + def __call__(self, **kwargs: Any) -> T: ... + + +class Test(Generic[T]): + def __init__(self, f: Factory[T]) -> None: + ... + +t = Test(Mock()) +reveal_type(t) # N: Revealed type is "__main__.Test[Any]" +[builtins fixtures/dict.pyi] From c20fd7838338cd65d6c7c6e252eda85996cfc98e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 5 Jun 2025 15:09:36 +0100 Subject: [PATCH 17/20] Handle assignment of bound methods in class bodies (#19233) Fixes https://github.com/python/mypy/issues/18438 Fixes https://github.com/python/mypy/issues/19146 Surprisingly, a very small change is sufficient to replicate Python runtime behavior for all the important cases (see `checkmember.py`). I also replace the `bound_args` argument of `CallableType`, that was mostly unused, with a flag (as suggested by @JukkaL) and make sure it is properly set/preserved everywhere. --- mypy/checker.py | 2 +- mypy/checkexpr.py | 2 +- mypy/checkmember.py | 4 +-- mypy/fixup.py | 3 -- mypy/messages.py | 4 +-- mypy/server/astdiff.py | 1 + mypy/typeops.py | 3 +- mypy/types.py | 20 ++++++------- test-data/unit/check-classes.test | 2 +- test-data/unit/check-functions.test | 42 +++++++++++++++++++++++++++ test-data/unit/check-incremental.test | 24 +++++++++++++++ test-data/unit/fine-grained.test | 24 +++++++++++++++ 12 files changed, 110 insertions(+), 21 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index cb465b390aa6..5a5d19cc0006 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2448,7 +2448,7 @@ def erase_override(t: Type) -> Type: if not is_subtype(original_arg_type, erase_override(override_arg_type)): context: Context = node if isinstance(node, FuncDef) and not node.is_property: - arg_node = node.arguments[i + len(override.bound_args)] + arg_node = node.arguments[i + override.bound()] if arg_node.line != -1: context = arg_node self.msg.argument_incompatible_with_supertype( diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f375c14c5fc4..df0e9bdaedde 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4970,7 +4970,7 @@ def apply_type_arguments_to_callable( tp.fallback, name="tuple", definition=tp.definition, - bound_args=tp.bound_args, + is_bound=tp.is_bound, ) self.msg.incompatible_type_application( min_arg_count, len(type_vars), len(args), ctx diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 4821c622cb0c..ea76a6cc304f 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -916,7 +916,7 @@ def analyze_var( bound_items = [] for ct in call_type.items if isinstance(call_type, UnionType) else [call_type]: p_ct = get_proper_type(ct) - if isinstance(p_ct, FunctionLike) and not p_ct.is_type_obj(): + if isinstance(p_ct, FunctionLike) and (not p_ct.bound() or var.is_property): item = expand_and_bind_callable(p_ct, var, itype, name, mx, is_trivial_self) else: item = expand_without_binding(ct, var, itype, original_itype, mx) @@ -1503,6 +1503,6 @@ def bind_self_fast(method: F, original_type: Type | None = None) -> F: arg_types=method.arg_types[1:], arg_kinds=method.arg_kinds[1:], arg_names=method.arg_names[1:], - bound_args=[original_type], + is_bound=True, ) return cast(F, res) diff --git a/mypy/fixup.py b/mypy/fixup.py index 8e7cd40544bf..0e9c186fd42a 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -271,9 +271,6 @@ def visit_callable_type(self, ct: CallableType) -> None: ct.ret_type.accept(self) for v in ct.variables: v.accept(self) - for arg in ct.bound_args: - if arg: - arg.accept(self) if ct.type_guard is not None: ct.type_guard.accept(self) if ct.type_is is not None: diff --git a/mypy/messages.py b/mypy/messages.py index 2e07d7f63498..47e7d20dde78 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -644,8 +644,8 @@ def incompatible_argument( callee_name = callable_name(callee) if callee_name is not None: name = callee_name - if callee.bound_args and callee.bound_args[0] is not None: - base = format_type(callee.bound_args[0], self.options) + if object_type is not None: + base = format_type(object_type, self.options) else: base = extract_type(name) diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 1b0cc218ed16..16a0d882a8aa 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -460,6 +460,7 @@ def visit_callable_type(self, typ: CallableType) -> SnapshotItem: typ.is_type_obj(), typ.is_ellipsis_args, snapshot_types(typ.variables), + typ.is_bound, ) def normalize_callable_variables(self, typ: CallableType) -> CallableType: diff --git a/mypy/typeops.py b/mypy/typeops.py index 3715081ae173..1a29054513a2 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -185,6 +185,7 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P arg_kinds=[ARG_STAR, ARG_STAR2], arg_names=["_args", "_kwds"], ret_type=any_type, + is_bound=True, fallback=named_type("builtins.function"), ) return class_callable(sig, info, fallback, None, is_new=False) @@ -479,7 +480,7 @@ class B(A): pass arg_kinds=func.arg_kinds[1:], arg_names=func.arg_names[1:], variables=variables, - bound_args=[original_type], + is_bound=True, ) return cast(F, res) diff --git a/mypy/types.py b/mypy/types.py index 41a958ae93cc..7f67c303a929 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1597,6 +1597,9 @@ def with_name(self, name: str) -> FunctionLike: def get_name(self) -> str | None: pass + def bound(self) -> bool: + return bool(self.items) and self.items[0].is_bound + class FormalArgument(NamedTuple): name: str | None @@ -1826,8 +1829,7 @@ class CallableType(FunctionLike): # 'dict' and 'partial' for a `functools.partial` evaluation) "from_type_type", # Was this callable generated by analyzing Type[...] # instantiation? - "bound_args", # Bound type args, mostly unused but may be useful for - # tools that consume mypy ASTs + "is_bound", # Is this a bound method? "def_extras", # Information about original definition we want to serialize. # This is used for more detailed error messages. "type_guard", # T, if -> TypeGuard[T] (ret_type is bool in this case). @@ -1855,7 +1857,7 @@ def __init__( implicit: bool = False, special_sig: str | None = None, from_type_type: bool = False, - bound_args: Sequence[Type | None] = (), + is_bound: bool = False, def_extras: dict[str, Any] | None = None, type_guard: Type | None = None, type_is: Type | None = None, @@ -1888,9 +1890,7 @@ def __init__( self.from_type_type = from_type_type self.from_concatenate = from_concatenate self.imprecise_arg_kinds = imprecise_arg_kinds - if not bound_args: - bound_args = () - self.bound_args = bound_args + self.is_bound = is_bound if def_extras: self.def_extras = def_extras elif isinstance(definition, FuncDef): @@ -1927,7 +1927,7 @@ def copy_modified( implicit: Bogus[bool] = _dummy, special_sig: Bogus[str | None] = _dummy, from_type_type: Bogus[bool] = _dummy, - bound_args: Bogus[list[Type | None]] = _dummy, + is_bound: Bogus[bool] = _dummy, def_extras: Bogus[dict[str, Any]] = _dummy, type_guard: Bogus[Type | None] = _dummy, type_is: Bogus[Type | None] = _dummy, @@ -1952,7 +1952,7 @@ def copy_modified( implicit=implicit if implicit is not _dummy else self.implicit, special_sig=special_sig if special_sig is not _dummy else self.special_sig, from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type, - bound_args=bound_args if bound_args is not _dummy else self.bound_args, + is_bound=is_bound if is_bound is not _dummy else self.is_bound, def_extras=def_extras if def_extras is not _dummy else dict(self.def_extras), type_guard=type_guard if type_guard is not _dummy else self.type_guard, type_is=type_is if type_is is not _dummy else self.type_is, @@ -2277,7 +2277,7 @@ def serialize(self) -> JsonDict: "variables": [v.serialize() for v in self.variables], "is_ellipsis_args": self.is_ellipsis_args, "implicit": self.implicit, - "bound_args": [(None if t is None else t.serialize()) for t in self.bound_args], + "is_bound": self.is_bound, "def_extras": dict(self.def_extras), "type_guard": self.type_guard.serialize() if self.type_guard is not None else None, "type_is": (self.type_is.serialize() if self.type_is is not None else None), @@ -2300,7 +2300,7 @@ def deserialize(cls, data: JsonDict) -> CallableType: variables=[cast(TypeVarLikeType, deserialize_type(v)) for v in data["variables"]], is_ellipsis_args=data["is_ellipsis_args"], implicit=data["implicit"], - bound_args=[(None if t is None else deserialize_type(t)) for t in data["bound_args"]], + is_bound=data["is_bound"], def_extras=data["def_extras"], type_guard=( deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 59204cc31c43..036c8e21c310 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -4292,7 +4292,7 @@ int.__eq__(3, 4) [builtins fixtures/args.pyi] [out] main:33: error: Too few arguments for "__eq__" of "int" -main:33: error: Unsupported operand types for == ("int" and "Type[int]") +main:33: error: Unsupported operand types for == ("Type[int]" and "Type[int]") [case testDupBaseClasses] class A: diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index ac93c6c20354..35d76a65ccc3 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3599,3 +3599,45 @@ class Bar(Foo): def foo(self, value: Union[int, str]) -> Union[int, str]: return super().foo(value) # E: Call to abstract method "foo" of "Foo" with trivial body via super() is unsafe + +[case testBoundMethodsAssignedInClassBody] +from typing import Callable + +class A: + def f(self, x: int) -> str: + pass + @classmethod + def g(cls, x: int) -> str: + pass + @staticmethod + def h(x: int) -> str: + pass + attr: Callable[[int], str] + +class C: + x1 = A.f + x2 = A.g + x3 = A().f + x4 = A().g + x5 = A.h + x6 = A().h + x7 = A().attr + +reveal_type(C.x1) # N: Revealed type is "def (self: __main__.A, x: builtins.int) -> builtins.str" +reveal_type(C.x2) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x3) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x4) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x5) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x6) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C.x7) # N: Revealed type is "def (builtins.int) -> builtins.str" + +reveal_type(C().x1) # E: Invalid self argument "C" to attribute function "x1" with type "Callable[[A, int], str]" \ + # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x2) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x3) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x4) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x5) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x6) # N: Revealed type is "def (x: builtins.int) -> builtins.str" +reveal_type(C().x7) # E: Invalid self argument "C" to attribute function "x7" with type "Callable[[int], str]" \ + # N: Revealed type is "def () -> builtins.str" +[builtins fixtures/classmethod.pyi] diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 9d5902246ae5..fabd7e294d9a 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6862,3 +6862,27 @@ if int(): [out] [out2] main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testMethodMakeBoundIncremental] +from a import A +a = A() +a.f() +[file a.py] +class B: + def f(self, s: A) -> int: ... + +def f(s: A) -> int: ... + +class A: + f = f +[file a.py.2] +class B: + def f(self, s: A) -> int: ... + +def f(s: A) -> int: ... + +class A: + f = B().f +[out] +[out2] +main:3: error: Too few arguments diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index df244b3135e9..eb07f37a8671 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -11217,3 +11217,27 @@ class A: [out] == main:3: error: Property "f" defined in "A" is read-only + +[case testMethodMakeBoundFineGrained] +from a import A +a = A() +a.f() +[file a.py] +class B: + def f(self, s: A) -> int: ... + +def f(s: A) -> int: ... + +class A: + f = f +[file a.py.2] +class B: + def f(self, s: A) -> int: ... + +def f(s: A) -> int: ... + +class A: + f = B().f +[out] +== +main:3: error: Too few arguments From 9fb5ff66c51bd971d7a6b1260cc0ec9f1b82cc06 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Jun 2025 23:59:23 +0100 Subject: [PATCH 18/20] Fix properties with setters after deleters (#19248) Fixes https://github.com/python/mypy/issues/19224 Note we must add an additional attribute on `OverloadedFuncDef` since decorator expressions are not serialized. --- mypy/checker.py | 10 ++++------ mypy/checkmember.py | 4 ++-- mypy/nodes.py | 24 +++++++++++++++++++++++- mypy/semanal.py | 1 + test-data/unit/check-classes.test | 20 ++++++++++++++++++++ 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5a5d19cc0006..dc3d670dd73e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -675,11 +675,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: assert isinstance(defn.items[0], Decorator) self.visit_decorator(defn.items[0]) if defn.items[0].var.is_settable_property: - # TODO: here and elsewhere we assume setter immediately follows getter. - assert isinstance(defn.items[1], Decorator) # Perform a reduced visit just to infer the actual setter type. - self.visit_decorator_inner(defn.items[1], skip_first_item=True) - setter_type = defn.items[1].var.type + self.visit_decorator_inner(defn.setter, skip_first_item=True) + setter_type = defn.setter.var.type # Check if the setter can accept two positional arguments. any_type = AnyType(TypeOfAny.special_form) fallback_setter_type = CallableType( @@ -690,7 +688,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: fallback=self.named_type("builtins.function"), ) if setter_type and not is_subtype(setter_type, fallback_setter_type): - self.fail("Invalid property setter signature", defn.items[1].func) + self.fail("Invalid property setter signature", defn.setter.func) setter_type = self.extract_callable_type(setter_type, defn) if not isinstance(setter_type, CallableType) or len(setter_type.arg_types) != 2: # TODO: keep precise type for callables with tricky but valid signatures. @@ -2149,7 +2147,7 @@ def check_setter_type_override(self, defn: OverloadedFuncDef, base: TypeInfo) -> assert typ is not None and original_type is not None if not is_subtype(original_type, typ): - self.msg.incompatible_setter_override(defn.items[1], typ, original_type, base) + self.msg.incompatible_setter_override(defn.setter, typ, original_type, base) def check_method_override_for_base_with_name( self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo diff --git a/mypy/checkmember.py b/mypy/checkmember.py index ea76a6cc304f..596eccab09a7 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -341,8 +341,8 @@ def analyze_instance_member_access( assert isinstance(method, OverloadedFuncDef) getter = method.items[0] assert isinstance(getter, Decorator) - if mx.is_lvalue and (len(items := method.items) > 1): - mx.chk.warn_deprecated(items[1], mx.context) + if mx.is_lvalue and getter.var.is_settable_property: + mx.chk.warn_deprecated(method.setter, mx.context) return analyze_var(name, getter.var, typ, mx) if mx.is_lvalue and not mx.suppress_errors: diff --git a/mypy/nodes.py b/mypy/nodes.py index 9a9403722a16..0d4c96ac5793 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -550,12 +550,20 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement): Overloaded variants must be consecutive in the source file. """ - __slots__ = ("items", "unanalyzed_items", "impl", "deprecated", "_is_trivial_self") + __slots__ = ( + "items", + "unanalyzed_items", + "impl", + "deprecated", + "setter_index", + "_is_trivial_self", + ) items: list[OverloadPart] unanalyzed_items: list[OverloadPart] impl: OverloadPart | None deprecated: str | None + setter_index: int | None def __init__(self, items: list[OverloadPart]) -> None: super().__init__() @@ -563,6 +571,7 @@ def __init__(self, items: list[OverloadPart]) -> None: self.unanalyzed_items = items.copy() self.impl = None self.deprecated = None + self.setter_index = None self._is_trivial_self: bool | None = None if items: # TODO: figure out how to reliably set end position (we don't know the impl here). @@ -598,6 +607,17 @@ def is_trivial_self(self) -> bool: self._is_trivial_self = True return True + @property + def setter(self) -> Decorator: + # Do some consistency checks first. + first_item = self.items[0] + assert isinstance(first_item, Decorator) + assert first_item.var.is_settable_property + assert self.setter_index is not None + item = self.items[self.setter_index] + assert isinstance(item, Decorator) + return item + def accept(self, visitor: StatementVisitor[T]) -> T: return visitor.visit_overloaded_func_def(self) @@ -610,6 +630,7 @@ def serialize(self) -> JsonDict: "impl": None if self.impl is None else self.impl.serialize(), "flags": get_flags(self, FUNCBASE_FLAGS), "deprecated": self.deprecated, + "setter_index": self.setter_index, } @classmethod @@ -630,6 +651,7 @@ def deserialize(cls, data: JsonDict) -> OverloadedFuncDef: res._fullname = data["fullname"] set_flags(res, data["flags"]) res.deprecated = data["deprecated"] + res.setter_index = data["setter_index"] # NOTE: res.info will be set in the fixup phase. return res diff --git a/mypy/semanal.py b/mypy/semanal.py index 89bb5ab97c2a..bc64e7e7ca53 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1558,6 +1558,7 @@ def analyze_property_with_multi_part_definition( ) assert isinstance(setter_func_type, CallableType) bare_setter_type = setter_func_type + defn.setter_index = i + 1 if first_node.name == "deleter": item.func.abstract_status = first_item.func.abstract_status for other_node in item.decorators[1:]: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 036c8e21c310..10e15139218f 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8739,3 +8739,23 @@ class NoopPowerResource: def hardware_type(self) -> None: # E: Invalid property setter signature self.hardware_type = None # Note: intentionally recursive [builtins fixtures/property.pyi] + +[case testPropertyAllowsDeleterBeforeSetter] +class C: + @property + def foo(self) -> str: ... + @foo.deleter + def foo(self) -> None: ... + @foo.setter + def foo(self, val: int) -> None: ... + + @property + def bar(self) -> int: ... + @bar.deleter + def bar(self) -> None: ... + @bar.setter + def bar(self, value: int, val: int) -> None: ... # E: Invalid property setter signature + +C().foo = "no" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +C().bar = "fine" +[builtins fixtures/property.pyi] From e253eded9c887630f3f5404c4b9f73f13570476a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 11 Jun 2025 13:57:31 +0900 Subject: [PATCH 19/20] Single underscore is not a sunder (#19273) Fixes https://github.com/python/mypy/issues/19271. --- mypy/util.py | 2 +- test-data/unit/check-enum.test | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/mypy/util.py b/mypy/util.py index d3f49f74bbae..d7ff2a367fa2 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -66,7 +66,7 @@ def is_dunder(name: str, exclude_special: bool = False) -> bool: def is_sunder(name: str) -> bool: - return not is_dunder(name) and name.startswith("_") and name.endswith("_") + return not is_dunder(name) and name.startswith("_") and name.endswith("_") and name != "_" def split_module_names(mod_name: str) -> list[str]: diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index cc9048db18dc..3001bc9a69cb 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -2524,3 +2524,18 @@ class Base: self.o = Enum("o", names) # E: Enum type as attribute is not supported \ # E: Second argument of Enum() must be string, tuple, list or dict literal for mypy to determine Enum members [builtins fixtures/tuple.pyi] + +[case testSingleUnderscoreNameEnumMember] +# flags: --warn-unreachable + +# https://github.com/python/mypy/issues/19271 +from enum import Enum + +class Things(Enum): + _ = "under score" + +def check(thing: Things) -> None: + if thing is Things._: + return None + return None # E: Statement is unreachable +[builtins fixtures/enum.pyi] From 68b8fa097d080c92d30a429bc74de8acd56caf85 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 Jun 2025 16:25:56 +0100 Subject: [PATCH 20/20] Bump version to 1.16.1 --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index d2d42a386dec..a003b2b70558 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.16.1+dev" +__version__ = "1.16.1" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))