Skip to content

Make tuple() constructor return structural Type::Tuple#2682

Closed
yangdanny97 wants to merge 1 commit intofacebook:mainfrom
yangdanny97:export-D95478317
Closed

Make tuple() constructor return structural Type::Tuple#2682
yangdanny97 wants to merge 1 commit intofacebook:mainfrom
yangdanny97:export-D95478317

Conversation

@yangdanny97
Copy link
Contributor

Summary:
Calling tuple() previously returned a nominal Type::ClassType(tuple[T]), which many downstream operations (concat, starred unpacking, except clause decomposition, etc.) didn't recognize because they only pattern-match on Type::Tuple. This forced at least 8 call sites to normalize via as_tuple(), and operations that didn't normalize were subtly broken.

Fix this at the source: post-process construct_class to convert the return type to Type::Tuple(Tuple::Unbounded(T)) when the class is exactly builtins.tuple. This uses is_builtin("tuple") so it only fires for the exact builtin, not for NamedTuples or tuple subclasses, which correctly remain as nominal ClassType.

The existing as_tuple() normalizations downstream are kept because they still serve NamedTuples and tuple subclasses — they just become no-ops for plain tuple() calls.

Differential Revision: D95478317

Summary:
Calling `tuple()` previously returned a nominal `Type::ClassType(tuple[T])`, which many downstream operations (concat, starred unpacking, except clause decomposition, etc.) didn't recognize because they only pattern-match on `Type::Tuple`. This forced at least 8 call sites to normalize via `as_tuple()`, and operations that didn't normalize were subtly broken.

Fix this at the source: post-process `construct_class` to convert the return type to `Type::Tuple(Tuple::Unbounded(T))` when the class is exactly `builtins.tuple`. This uses `is_builtin("tuple")` so it only fires for the exact builtin, not for NamedTuples or tuple subclasses, which correctly remain as nominal `ClassType`.

The existing `as_tuple()` normalizations downstream are kept because they still serve NamedTuples and tuple subclasses — they just become no-ops for plain `tuple()` calls.

Differential Revision: D95478317
@meta-codesync
Copy link

meta-codesync bot commented Mar 6, 2026

@yangdanny97 has exported this pull request. If you are a Meta employee, you can view the originating Diff in D95478317.

@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Diff from mypy_primer, showing the effect of this PR on open source code:

hydpy (https://github.com/hydpy-dev/hydpy)
- ERROR hydpy/models/manager/manager_derived.py:285:49-287:14: No matching overload found for function `networkx.utils.backends._dispatchable.__call__` called with arguments: (DiGraph[Node], nodelist=tuple[Node, ...], dtype=type[bool]) [no-matching-overload]
+ ERROR hydpy/models/manager/manager_derived.py:285:49-287:14: No matching overload found for function `networkx.utils.backends._dispatchable.__call__` called with arguments: (DiGraph[Node], nodelist=tuple[Node, *tuple[Any, ...]], dtype=type[bool]) [no-matching-overload]

bokeh (https://github.com/bokeh/bokeh)
- ERROR src/bokeh/plotting/_renderer.py:313:12-62: Returned type `tuple[str, ...] | tuple[str, None]` is not assignable to declared return type `tuple[str, str | None]` [bad-return]
+ ERROR src/bokeh/plotting/_renderer.py:313:12-62: Returned type `tuple[str, None] | tuple[str, ...]` is not assignable to declared return type `tuple[str, str | None]` [bad-return]

dedupe (https://github.com/dedupeio/dedupe)
- ERROR dedupe/api.py:998:9-31: Overload return type `Iterable[tuple[int, tuple[tuple[int, float], ...]]]` is not assignable to implementation return type `Generator[tuple[Unknown, tuple[tuple[RecordID, float], ...]] | tuple[Unknown, tuple[()]], Unknown]` [inconsistent-overload]
+ ERROR dedupe/api.py:998:9-31: Overload return type `Iterable[tuple[int, tuple[tuple[int, float], ...]]]` is not assignable to implementation return type `Generator[tuple[Unknown, tuple[()]] | tuple[Unknown, tuple[tuple[RecordID, float], ...]], Unknown]` [inconsistent-overload]
- ERROR dedupe/api.py:1003:9-31: Overload return type `Iterable[tuple[str, tuple[tuple[str, float], ...]]]` is not assignable to implementation return type `Generator[tuple[Unknown, tuple[tuple[RecordID, float], ...]] | tuple[Unknown, tuple[()]], Unknown]` [inconsistent-overload]
+ ERROR dedupe/api.py:1003:9-31: Overload return type `Iterable[tuple[str, tuple[tuple[str, float], ...]]]` is not assignable to implementation return type `Generator[tuple[Unknown, tuple[()]] | tuple[Unknown, tuple[tuple[RecordID, float], ...]], Unknown]` [inconsistent-overload]

pydantic (https://github.com/pydantic/pydantic)
- ERROR pydantic/v1/generics.py:124:25-132:14: No matching overload found for function `pydantic.v1.main.create_model` called with arguments: (str, __module__=str, __base__=tuple[type[GenericModelT], ...], __config__=None, __validators__=dict[str, classmethod[Any, Ellipsis, Any] | classmethod[Any, Ellipsis, Unknown]], __cls_kwargs__=None, **dict[Unknown, tuple[DeferredType, FieldInfo]]) [no-matching-overload]
+ ERROR pydantic/v1/generics.py:124:25-132:14: No matching overload found for function `pydantic.v1.main.create_model` called with arguments: (str, __module__=str, __base__=tuple[type[GenericModelT], *tuple[type[Any], ...]], __config__=None, __validators__=dict[str, classmethod[Any, Ellipsis, Any] | classmethod[Any, Ellipsis, Unknown]], __cls_kwargs__=None, **dict[Unknown, tuple[DeferredType, FieldInfo]]) [no-matching-overload]

vision (https://github.com/pytorch/vision)
- ERROR torchvision/ops/poolers.py:283:34-45: Argument `int | list[int] | tuple[int]` is not assignable to parameter `iterable` with type `Iterable[Unknown]` in function `tuple.__new__` [bad-argument-type]
- ERROR torchvision/transforms/_functional_pil.py:172:19-29: `int | object` is not assignable to variable `padding` with type `int | list[int] | tuple[int, ...]` [bad-assignment]
- ERROR torchvision/transforms/_functional_pil.py:181:49-56: Argument `int | list[int] | tuple[int, ...]` is not assignable to parameter `border` with type `int | tuple[int, ...]` in function `PIL.ImageOps.expand` [bad-argument-type]
- ERROR torchvision/transforms/_functional_pil.py:185:44-51: Argument `int | list[int] | tuple[int, ...]` is not assignable to parameter `border` with type `int | tuple[int, ...]` in function `PIL.ImageOps.expand` [bad-argument-type]
- ERROR torchvision/transforms/_functional_pil.py:199:31-37: No matching overload found for function `numpy._typing._ufunc._UFunc_Nin2_Nout1.__call__` called with arguments: (list[int | object], Literal[0]) [no-matching-overload]
- ERROR torchvision/transforms/_functional_pil.py:205:62-68: No matching overload found for function `numpy._typing._ufunc._UFunc_Nin2_Nout1.__call__` called with arguments: (list[int | object], Literal[0]) [no-matching-overload]
- ERROR torchvision/transforms/_functional_pil.py:210:25-97: No matching overload found for function `numpy.lib._arraypad_impl.pad` called with arguments: (Image, tuple[tuple[Unknown, Unknown], tuple[Unknown, Unknown]], mode=Literal['edge', 'reflect', 'symmetric']) [no-matching-overload]
+ ERROR torchvision/transforms/_functional_pil.py:210:25-97: No matching overload found for function `numpy.lib._arraypad_impl.pad` called with arguments: (Image, tuple[tuple[Any, Any], tuple[Any, Any]], mode=Literal['edge', 'reflect', 'symmetric']) [no-matching-overload]
- ERROR torchvision/transforms/_functional_pil.py:218:25-100: No matching overload found for function `numpy.lib._arraypad_impl.pad` called with arguments: (Image, tuple[tuple[Unknown, Unknown], tuple[Unknown, Unknown], tuple[Literal[0], Literal[0]]], Literal['edge', 'reflect', 'symmetric']) [no-matching-overload]
+ ERROR torchvision/transforms/_functional_pil.py:218:25-100: No matching overload found for function `numpy.lib._arraypad_impl.pad` called with arguments: (Image, tuple[tuple[Any, Any], tuple[Any, Any], tuple[Literal[0], Literal[0]]], Literal['edge', 'reflect', 'symmetric']) [no-matching-overload]
- ERROR torchvision/transforms/_functional_pil.py:221:25-92: No matching overload found for function `numpy.lib._arraypad_impl.pad` called with arguments: (Image | Unknown, tuple[tuple[Unknown, Unknown], tuple[Unknown, Unknown]], Literal['edge', 'reflect', 'symmetric']) [no-matching-overload]
+ ERROR torchvision/transforms/_functional_pil.py:221:25-92: No matching overload found for function `numpy.lib._arraypad_impl.pad` called with arguments: (Image | Unknown, tuple[tuple[Any, Any], tuple[Any, Any]], Literal['edge', 'reflect', 'symmetric']) [no-matching-overload]

setuptools (https://github.com/pypa/setuptools)
- ERROR setuptools/_distutils/_modified.py:61:12-59: Returned type `tuple[list[Unknown], ...] | tuple[list[_SourcesT], list[_TargetsT]]` is not assignable to declared return type `tuple[list[_SourcesT], list[_TargetsT]]` [bad-return]
+ ERROR setuptools/_distutils/_modified.py:61:12-59: Returned type `tuple[list[_SourcesT], list[_TargetsT]] | tuple[list[Unknown], ...]` is not assignable to declared return type `tuple[list[_SourcesT], list[_TargetsT]]` [bad-return]

jax (https://github.com/google/jax)
+ ERROR jax/experimental/mosaic/gpu/fragmented_array.py:2434:20-84: Expected an iterable, got `tuple[Literal[False], ...] | tuple[Literal[True] | Unknown, ...]` [not-iterable]

mypy (https://github.com/python/mypy)
- ERROR mypy/modulefinder.py:40:37-67: No matching overload found for function `map.__new__` called with arguments: (type[map[Unknown]], Overload[
-   [AnyStr: (str, bytes)](path: PathLike[AnyStr]) -> AnyStr
-   [AnyStr: (str, bytes)](path: AnyStr) -> AnyStr
- ], tuple[str, ...]) [no-matching-overload]
-   [AnyStr: (str, bytes)](path: PathLike[AnyStr]) -> AnyStr
-   [AnyStr: (str, bytes)](path: AnyStr) -> AnyStr
- ], tuple[str, ...]) [no-matching-overload]
- ERROR mypy/modulefinder.py:42:35-63: No matching overload found for function `map.__new__` called with arguments: (type[map[Unknown]], Overload[
- ERROR mypy/modulefinder.py:44:38-69: No matching overload found for function `map.__new__` called with arguments: (type[map[Unknown]], Overload[
-   [AnyStr: (str, bytes)](path: PathLike[AnyStr]) -> AnyStr
-   [AnyStr: (str, bytes)](path: AnyStr) -> AnyStr
- ], tuple[str, ...]) [no-matching-overload]
-   [AnyStr: (str, bytes)](path: PathLike[AnyStr]) -> AnyStr
-   [AnyStr: (str, bytes)](path: AnyStr) -> AnyStr
- ], tuple[str, ...]) [no-matching-overload]
- ERROR mypy/modulefinder.py:46:39-71: No matching overload found for function `map.__new__` called with arguments: (type[map[Unknown]], Overload[

@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Primer Diff Classification

❌ 1 regression(s) | ➖ 6 neutral | 7 project(s) total

1 regression(s) across jax. error kinds: not-iterable. caused by construct_class().

Project Verdict Changes Error Kinds Root Cause
hydpy ➖ Neutral +1, -1 no-matching-overload
bokeh ➖ Neutral +1, -1 bad-return
dedupe ➖ Neutral +2, -2 inconsistent-overload
pydantic ➖ Neutral +1, -1 no-matching-overload
vision ➖ Neutral +3, -9 removed-false-positives construct_class()
setuptools ➖ Neutral +1, -1 bad-return
jax ❌ Regression +1 not-iterable construct_class()
Detailed analysis

❌ Regression (1)

jax (+1)

This is a regression. The PR changed how tuple() constructors are handled, converting them from nominal ClassType(tuple[T]) to structural Type::Tuple(Tuple::Unbounded(T)). However, this change appears to have broken pyrefly's ability to recognize that union types containing only tuple types are still iterable for unpacking purposes. The code *(a in axis for a in range(untiled_rank)) creates a generator that yields boolean values, and tuple() constructor calls on this should produce an iterable type that can be unpacked. The error suggests pyrefly is creating a union type tuple[Literal[False], ...] | tuple[Literal[True] | Unknown, ...] but then failing to recognize this union as iterable, even though both sides are tuple types and therefore iterable. This is a type checker limitation rather than a real bug in the code.
Attribution: The change to construct_class() in pyrefly/lib/alt/call.rs that converts tuple() constructor calls to structural Type::Tuple is causing the type inference to produce a union type that pyrefly's iterable checking doesn't handle properly.

➖ Neutral (6)

hydpy (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

bokeh (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

dedupe (+2, -2)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

pydantic (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

vision (+3, -9)

removed-false-positives: The removed bad-argument-type and bad-assignment errors were incorrect - tuple() accepts iterables and tuple indexing returns the element type
new-numpy-overload-issues: The new no-matching-overload errors with numpy.pad appear to be false positives caused by overload resolution not handling the new structural tuple representation correctly

Overall: This is a mixed outcome. The PR fixed some false positives by improving tuple type representation, but introduced new false positives with numpy overloads. The removed errors were genuinely wrong - line 283 in poolers.py shows tuple(output_size) where output_size is Union[int, tuple[int], list[int]], and tuple() accepts any iterable. Line 172 shows padding = padding[0] where padding is tuple[int, ...] after the isinstance check, so padding[0] is definitely an int. However, the new numpy.pad errors appear to be false positives - numpy.pad accepts tuple arguments for padding specification, but pyrefly's overload resolution is now failing to match them correctly due to the structural vs nominal tuple representation change.

Attribution: The change to construct_class() in pyrefly/lib/alt/call.rs modified how tuple() constructor calls are handled. Previously they returned nominal Type::ClassType(tuple[T]), now they return structural Type::Tuple(Tuple::Unbounded(T)) for the builtin tuple class specifically. This improved type inference for tuple operations but created new overload resolution issues with numpy functions.

setuptools (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

Suggested Fix

Summary: The PR changed tuple() constructor calls to return structural Type::Tuple instead of nominal ClassType(tuple[T]), which broke union type iterable checking in jax.

1. In construct_class() in pyrefly/lib/alt/call.rs, modify the tuple normalization logic to preserve union types containing only tuple types as iterable. Add a check: when the result would be a union of structural tuples, keep it as a union of ClassType(tuple[T]) instances instead of converting to structural Type::Tuple, so that union iterable checking continues to work.

Files: pyrefly/lib/alt/call.rs
Confidence: high
Affected projects: jax
Fixes: bad-assignment
The regression is specifically caused by the new tuple normalization logic converting tuple() constructor results to structural Type::Tuple. This breaks pyrefly's ability to recognize unions of tuple types as iterable for unpacking. The fix should preserve the iterable nature of tuple unions while keeping the structural benefits for non-union cases. Expected outcome: eliminates 1 bad-assignment error in jax where tuple() constructor results in generator expressions can be unpacked.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (5 heuristic, 2 LLM)

@meta-codesync meta-codesync bot closed this in 13849da Mar 7, 2026
@meta-codesync
Copy link

meta-codesync bot commented Mar 7, 2026

This pull request has been merged in 13849da.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants