From 2f2c391e98f653fe3ff863b5ef074a5876eaa625 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 9 Jul 2025 01:18:35 +0200 Subject: [PATCH 1/8] Only do this for collection literals --- mypy/checkexpr.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8223ccfe4ca0..1108ba4628dc 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5079,10 +5079,19 @@ def fast_container_type( self.resolved_type[e] = NoneType() return None values.append(self.accept(item)) - vt = join.join_type_list(values) - if not allow_fast_container_literal(vt): - self.resolved_type[e] = NoneType() - return None + + values = [v for i, v in enumerate(values) if v not in values[:i]] + if len(values) == 1: + # If only one non-duplicate item remains, there's no need running whole + # inference cycle over it. This helps in pathological cases where items + # are complex overloads. + # https://github.com/python/mypy/issues/14718 + vt = values[0] + else: + vt = join.join_type_list(values) + if not allow_fast_container_literal(vt): + self.resolved_type[e] = NoneType() + return None ct = self.chk.named_generic_type(container_fullname, [vt]) self.resolved_type[e] = ct return ct From 48cb9e194b1b7da7ad8ed181ff1ba1ace5b26add Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 9 Jul 2025 01:32:29 +0200 Subject: [PATCH 2/8] Sync tests --- test-data/unit/check-generics.test | 10 +++++----- test-data/unit/check-redefine2.test | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 0be9d918c69f..abeb5face26f 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -2929,8 +2929,8 @@ def mix(fs: List[Callable[[S], T]]) -> Callable[[S], List[T]]: def id(__x: U) -> U: ... fs = [id, id, id] -reveal_type(mix(fs)) # N: Revealed type is "def [S] (S`11) -> builtins.list[S`11]" -reveal_type(mix([id, id, id])) # N: Revealed type is "def [S] (S`13) -> builtins.list[S`13]" +reveal_type(mix(fs)) # N: Revealed type is "def [S] (S`2) -> builtins.list[S`2]" +reveal_type(mix([id, id, id])) # N: Revealed type is "def [S] (S`4) -> builtins.list[S`4]" [builtins fixtures/list.pyi] [case testInferenceAgainstGenericCurry] @@ -3118,11 +3118,11 @@ def dec4_bound(f: Callable[[I], List[T]]) -> Callable[[I], T]: reveal_type(dec1(lambda x: x)) # N: Revealed type is "def [T] (T`3) -> builtins.list[T`3]" reveal_type(dec2(lambda x: x)) # N: Revealed type is "def [S] (S`5) -> builtins.list[S`5]" reveal_type(dec3(lambda x: x[0])) # N: Revealed type is "def [S] (S`8) -> S`8" -reveal_type(dec4(lambda x: [x])) # N: Revealed type is "def [S] (S`12) -> S`12" +reveal_type(dec4(lambda x: [x])) # N: Revealed type is "def [S] (S`11) -> S`11" reveal_type(dec1(lambda x: 1)) # N: Revealed type is "def (builtins.int) -> builtins.list[builtins.int]" reveal_type(dec5(lambda x: x)) # N: Revealed type is "def (builtins.int) -> builtins.list[builtins.int]" -reveal_type(dec3(lambda x: x)) # N: Revealed type is "def [S] (S`20) -> builtins.list[S`20]" -reveal_type(dec4(lambda x: x)) # N: Revealed type is "def [T] (builtins.list[T`24]) -> T`24" +reveal_type(dec3(lambda x: x)) # N: Revealed type is "def [S] (S`19) -> builtins.list[S`19]" +reveal_type(dec4(lambda x: x)) # N: Revealed type is "def [T] (builtins.list[T`23]) -> T`23" dec4_bound(lambda x: x) # E: Value of type variable "I" of "dec4_bound" cannot be "list[T]" [builtins fixtures/list.pyi] diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 3523772611aa..1abe957240b5 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -1073,7 +1073,7 @@ def f() -> None: while int(): x = [x] - reveal_type(x) # N: Revealed type is "Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]]]]]]]" + reveal_type(x) # N: Revealed type is "Union[Any, builtins.list[Any]]" [case testNewRedefinePartialNoneEmptyList] # flags: --allow-redefinition-new --local-partial-types From 19f0260ace3cf120788eff7ffbc61eed362e1099 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Thu, 10 Jul 2025 04:51:17 +0200 Subject: [PATCH 3/8] Fix interaction with deferrals --- mypy/checkexpr.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1108ba4628dc..863c43e4bb4e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5064,11 +5064,16 @@ def fast_container_type( Limitations: - no active type context - no star expressions - - the joined type of all entries must be an Instance or Tuple type + - not after deferral + - either exactly one distinct type inside, + or the joined type of all entries must be an Instance or Tuple type """ ctx = self.type_context[-1] if ctx: return None + if self.chk.current_node_deferred: + # Guarantees that all items will be Any, we'll reject it anyway. + return None rt = self.resolved_type.get(e, None) if rt is not None: return rt if isinstance(rt, Instance) else None @@ -5078,11 +5083,13 @@ def fast_container_type( # fallback to slow path self.resolved_type[e] = NoneType() return None - values.append(self.accept(item)) - values = [v for i, v in enumerate(values) if v not in values[:i]] - if len(values) == 1: - # If only one non-duplicate item remains, there's no need running whole + typ = self.accept(item) + if typ not in values: + values.append(typ) + + if len(values) == 1 and not self.chk.current_node_deferred: + # If only one non-duplicate item remains, there's no need to run the whole # inference cycle over it. This helps in pathological cases where items # are complex overloads. # https://github.com/python/mypy/issues/14718 From 0cc75eb87d7f17a3456097158d6469d6313c859b Mon Sep 17 00:00:00 2001 From: STerliakov Date: Thu, 10 Jul 2025 13:41:50 +0200 Subject: [PATCH 4/8] Deduplicate constraints before solving --- mypy/constraints.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy/constraints.py b/mypy/constraints.py index 9eeea3cb2c26..187804d0e248 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -251,6 +251,7 @@ def infer_constraints_for_callable( ) c = infer_constraints(callee.arg_types[i], actual_type, SUPERTYPE_OF) constraints.extend(c) + if ( param_spec and not any(c.type_var == param_spec.id for c in constraints) @@ -270,6 +271,8 @@ def infer_constraints_for_callable( ), ) ) + + constraints = [c for i, c in enumerate(constraints) if c not in constraints[:i]] if any(isinstance(v, ParamSpecType) for v in callee.variables): # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) From 48bc430f7f2c471804ff954feb46d0cee1db12f9 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Thu, 10 Jul 2025 13:56:18 +0200 Subject: [PATCH 5/8] Sync test --- test-data/unit/check-typeddict.test | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index a068a63274ca..d28782bfba45 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1164,22 +1164,21 @@ fc(b) # E: Argument 1 to "fc" has incompatible type "B"; expected "C" from typing import TypedDict, TypeVar A = TypedDict('A', {'x': int}) B = TypedDict('B', {'x': int}, total=False) +B2 = TypedDict('B2', {'x': int}, total=False) C = TypedDict('C', {'x': int, 'y': str}, total=False) +C2 = TypedDict('C2', {'x': int, 'y': str}, total=False) T = TypeVar('T') def j(x: T, y: T) -> T: return x a: A b: B +b2: B2 c: C -reveal_type(j(a, b)) \ - # N: Revealed type is "TypedDict({})" -reveal_type(j(b, b)) \ - # N: Revealed type is "TypedDict({'x'?: builtins.int})" -reveal_type(j(c, c)) \ - # N: Revealed type is "TypedDict({'x'?: builtins.int, 'y'?: builtins.str})" -reveal_type(j(b, c)) \ - # N: Revealed type is "TypedDict({'x'?: builtins.int})" -reveal_type(j(c, b)) \ - # N: Revealed type is "TypedDict({'x'?: builtins.int})" +c2: C2 +reveal_type(j(a, b)) # N: Revealed type is "TypedDict({})" +reveal_type(j(b, b2)) # N: Revealed type is "TypedDict({'x'?: builtins.int})" +reveal_type(j(c, c2)) # N: Revealed type is "TypedDict({'x'?: builtins.int, 'y'?: builtins.str})" +reveal_type(j(b, c)) # N: Revealed type is "TypedDict({'x'?: builtins.int})" +reveal_type(j(c, b)) # N: Revealed type is "TypedDict({'x'?: builtins.int})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] From ff26ebf4d7b39ddf523d24ba1f65e76638c042cf Mon Sep 17 00:00:00 2001 From: STerliakov Date: Thu, 10 Jul 2025 22:34:48 +0200 Subject: [PATCH 6/8] Use hash-based deduplication for constraints --- mypy/constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 187804d0e248..e9d970a0371e 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -272,7 +272,7 @@ def infer_constraints_for_callable( ) ) - constraints = [c for i, c in enumerate(constraints) if c not in constraints[:i]] + constraints = list(dict.fromkeys(constraints)) if any(isinstance(v, ParamSpecType) for v in callee.variables): # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) From b608a66f48a6399c66fdba31043c294fbb9d0bfb Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 18 Jul 2025 17:39:00 +0200 Subject: [PATCH 7/8] Revert constraint deduplication --- mypy/constraints.py | 3 --- test-data/unit/check-typeddict.test | 19 ++++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index e9d970a0371e..9eeea3cb2c26 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -251,7 +251,6 @@ def infer_constraints_for_callable( ) c = infer_constraints(callee.arg_types[i], actual_type, SUPERTYPE_OF) constraints.extend(c) - if ( param_spec and not any(c.type_var == param_spec.id for c in constraints) @@ -271,8 +270,6 @@ def infer_constraints_for_callable( ), ) ) - - constraints = list(dict.fromkeys(constraints)) if any(isinstance(v, ParamSpecType) for v in callee.variables): # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index d28782bfba45..a068a63274ca 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1164,21 +1164,22 @@ fc(b) # E: Argument 1 to "fc" has incompatible type "B"; expected "C" from typing import TypedDict, TypeVar A = TypedDict('A', {'x': int}) B = TypedDict('B', {'x': int}, total=False) -B2 = TypedDict('B2', {'x': int}, total=False) C = TypedDict('C', {'x': int, 'y': str}, total=False) -C2 = TypedDict('C2', {'x': int, 'y': str}, total=False) T = TypeVar('T') def j(x: T, y: T) -> T: return x a: A b: B -b2: B2 c: C -c2: C2 -reveal_type(j(a, b)) # N: Revealed type is "TypedDict({})" -reveal_type(j(b, b2)) # N: Revealed type is "TypedDict({'x'?: builtins.int})" -reveal_type(j(c, c2)) # N: Revealed type is "TypedDict({'x'?: builtins.int, 'y'?: builtins.str})" -reveal_type(j(b, c)) # N: Revealed type is "TypedDict({'x'?: builtins.int})" -reveal_type(j(c, b)) # N: Revealed type is "TypedDict({'x'?: builtins.int})" +reveal_type(j(a, b)) \ + # N: Revealed type is "TypedDict({})" +reveal_type(j(b, b)) \ + # N: Revealed type is "TypedDict({'x'?: builtins.int})" +reveal_type(j(c, c)) \ + # N: Revealed type is "TypedDict({'x'?: builtins.int, 'y'?: builtins.str})" +reveal_type(j(b, c)) \ + # N: Revealed type is "TypedDict({'x'?: builtins.int})" +reveal_type(j(c, b)) \ + # N: Revealed type is "TypedDict({'x'?: builtins.int})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] From 3aa947215415700fbe270cec6e71973e2c28e8fe Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 18 Jul 2025 17:52:27 +0200 Subject: [PATCH 8/8] Apply same treatment to dict literals --- mypy/checkexpr.py | 43 +++++++++++++++++++----------- test-data/unit/check-selftype.test | 2 +- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b8db2687689c..b34205552176 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5092,21 +5092,22 @@ def fast_container_type( if typ not in values: values.append(typ) - if len(values) == 1 and not self.chk.current_node_deferred: - # If only one non-duplicate item remains, there's no need to run the whole - # inference cycle over it. This helps in pathological cases where items - # are complex overloads. - # https://github.com/python/mypy/issues/14718 - vt = values[0] - else: - vt = join.join_type_list(values) - if not allow_fast_container_literal(vt): - self.resolved_type[e] = NoneType() - return None + vt = self._first_or_join_fast_item(values) + if vt is None: + self.resolved_type[e] = NoneType() + return None ct = self.chk.named_generic_type(container_fullname, [vt]) self.resolved_type[e] = ct return ct + def _first_or_join_fast_item(self, items: list[Type]) -> Type | None: + if len(items) == 1 and not self.chk.current_node_deferred: + return items[0] + typ = join.join_type_list(items) + if not allow_fast_container_literal(typ): + return None + return typ + def check_lst_expr(self, e: ListExpr | SetExpr | TupleExpr, fullname: str, tag: str) -> Type: # fast path t = self.fast_container_type(e, fullname) @@ -5293,13 +5294,23 @@ def fast_dict_type(self, e: DictExpr) -> Type | None: self.resolved_type[e] = NoneType() return None else: - keys.append(self.accept(key)) - values.append(self.accept(value)) - kt = join.join_type_list(keys) - vt = join.join_type_list(values) - if not (allow_fast_container_literal(kt) and allow_fast_container_literal(vt)): + key_t = self.accept(key) + if key_t not in keys: + keys.append(key_t) + value_t = self.accept(value) + if value_t not in values: + values.append(value_t) + + kt = self._first_or_join_fast_item(keys) + if kt is None: self.resolved_type[e] = NoneType() return None + + vt = self._first_or_join_fast_item(values) + if vt is None: + self.resolved_type[e] = NoneType() + return None + if stargs and (stargs[0] != kt or stargs[1] != vt): self.resolved_type[e] = NoneType() return None diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index 88ca53c8ed66..05c34eb70796 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -2018,7 +2018,7 @@ class Ben(Object): } @classmethod def doit(cls) -> Foo: - reveal_type(cls.MY_MAP) # N: Revealed type is "builtins.dict[builtins.str, def [Self <: __main__.Foo] (self: Self`4) -> Self`4]" + reveal_type(cls.MY_MAP) # N: Revealed type is "builtins.dict[builtins.str, def [Self <: __main__.Foo] (self: Self`1) -> Self`1]" foo_method = cls.MY_MAP["foo"] return foo_method(Foo()) [builtins fixtures/isinstancelist.pyi]