Skip to content

Fix inference when unpacking union type #19650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions mypy/argmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
TypedDictType,
TypeOfAny,
TypeVarTupleType,
UnionType,
UnpackType,
get_proper_type,
)
Expand Down Expand Up @@ -211,6 +212,18 @@ def expand_actual_type(
# Just return `Any`, other parts of code would raise
# a different error for improper use.
return AnyType(TypeOfAny.from_error)
elif isinstance(actual_type, UnionType):
item_types = [
self.expand_actual_type(
item,
actual_kind=actual_kind,
formal_name=formal_name,
formal_kind=formal_kind,
allow_unpack=allow_unpack,
)
for item in actual_type.items
]
Comment on lines +217 to +225
Copy link
Contributor

@randolf-scholz randolf-scholz Aug 13, 2025

Choose a reason for hiding this comment

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

This doesn't work because the mapper has mutable state (self.tuple_index), so one would need to create multiple copies of the current mapper and apply them independently.

But then this will run into problems if the union is of differently sized tuple, since then you end up with incongruent state across the different copies.

Solving this problem generally seems intractable due to combinatorial explosion: consider f(*x1, *x2, ..., *xn). If each $x_k$ is comprised of a union of $m_k$ differently sized tuples, then there are $m_1⋅m_2⋅…⋅m_k$ possible paths.

So at best I think one can do this union splitting if the Union doesn't contain tuples, or if all contained tuples are of equal length.

return UnionType.make_union(item_types)
elif isinstance(actual_type, TupleType):
# Get the next tuple item of a tuple *arg.
if self.tuple_index >= len(actual_type.items):
Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -3708,3 +3708,20 @@ foo(*args) # E: Argument 1 to "foo" has incompatible type "*list[object]"; expe
kwargs: dict[str, object]
foo(**kwargs) # E: Argument 1 to "foo" has incompatible type "**dict[str, object]"; expected "P"
[builtins fixtures/dict.pyi]

[case testUnpackUnionStarArgs]
from __future__ import annotations
from typing import TypeVar
T = TypeVar("T")

def f(*args: T) -> T: ...

def star_union_list(x: list[str | None] | list[str]):
reveal_type([*x]) # N: Revealed type is "builtins.list[Union[builtins.str, None]]"
reveal_type(f(*x)) # N: Revealed type is "Union[builtins.str, None]"

def star_union_list_tuple(x: list[str | None] | tuple[int, int]):
reveal_type([*x]) # N: Revealed type is "builtins.list[Union[builtins.str, None, builtins.int]]"
reveal_type(f(*x)) # N: Revealed type is "Union[builtins.str, None, builtins.int]"

[builtins fixtures/tuple.pyi]
Loading