Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5068,11 +5068,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
Expand All @@ -5082,15 +5087,27 @@ def fast_container_type(
# fallback to slow path
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):

typ = self.accept(item)
if typ not in values:
values.append(typ)

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit restrictive together with the above if. For example, if there is more than one callable type (like in the original example in the issue) this will not apply. Should we also proceed with fast path if each expression is a RefExpr (so that its type doesn't depend on the context).

Copy link
Collaborator Author

@sterliakov sterliakov Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but our join implementation for callables is horribly broken, so this will produce invalid results: e.g. we can confidently join two callables with named args with different names into one of them.

def left(*, x: str): ...
def right(*, y: str): ...

then join(left, right) is def (*, y: str).

I'm not sure if there are other similarly terrifying bugs there, so I'd really prefer to not expand this logic now. This is not related to expression source ([left, right] contains to RefExprs). I added a relevant todo here.

mypy/mypy/join.py

Lines 797 to 803 in 2996c91

def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
t, s = match_generic_callables(t, s)
arg_types: list[Type] = []
for i in range(len(t.arg_types)):
arg_types.append(safe_join(t.arg_types[i], s.arg_types[i]))
# TODO kinds and argument names

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I see, makes sense.

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)
Expand Down Expand Up @@ -5277,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
Expand Down
10 changes: 5 additions & 5 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -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`7) -> builtins.list[S`7]"
reveal_type(mix([id, id, id])) # N: Revealed type is "def [S] (S`9) -> builtins.list[S`9]"
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]
Expand Down Expand Up @@ -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]

Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-redefine2.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down