Skip to content

[ty] Preserve intersection receivers during attribute lookup#25626

Merged
charliermarsh merged 1 commit into
mainfrom
charlie/preserve-intersection-receivers
Jun 6, 2026
Merged

[ty] Preserve intersection receivers during attribute lookup#25626
charliermarsh merged 1 commit into
mainfrom
charlie/preserve-intersection-receivers

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

When looking up an attribute on an intersection, we search each positive element independently. We previously also used that individual element as the receiver when binding descriptors and Self, which meant we lost constraints:

def f(value: Intersection[A, B]):
    reveal_type(value.method())  # A, previously
    reveal_type(value.descriptor)  # A, previously

This change separates (1) the type whose members are searched from (2) the receiver used to bind the resulting attribute. We continue searching each intersection element independently, but bind descriptors and Self using the full intersection:

def f(value: Intersection[A, B]):
    reveal_type(value.method())  # A & B
    reveal_type(value.descriptor)  # A & B

Closes astral-sh/ty#3565.

Closes astral-sh/ty#3600.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Jun 4, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jun 4, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 92.16%. The percentage of expected errors that received a diagnostic held steady at 87.31%. The number of fully passing files held steady at 92/134.

@astral-sh-bot

astral-sh-bot Bot commented Jun 4, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 612.58MB 614.00MB +0.23% (1.42MB)
sphinx 223.17MB 223.78MB +0.27% (620.48kB)
trio 94.38MB 94.55MB +0.17% (168.59kB)
flake8 37.48MB 37.51MB +0.09% (33.62kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
member_lookup_with_policy_inner::interned_arguments 5.84MB 6.79MB +16.12% (964.88kB)
member_lookup_with_policy_inner 16.26MB 16.38MB +0.76% (126.93kB)
Type<'db>::apply_specialization_inner_ 3.09MB 3.18MB +2.89% (91.50kB)
when_constraint_set_assignable_to_owned_impl 3.03MB 3.11MB +2.53% (78.52kB)
Type<'db>::apply_specialization_inner_::interned_arguments 2.74MB 2.78MB +1.65% (46.09kB)
try_call_dunder_get_inner 1.24MB 1.26MB +2.09% (26.54kB)
Specialization 2.61MB 2.62MB +0.72% (19.33kB)
BoundMethodType 1.65MB 1.67MB +1.13% (19.14kB)
BoundMethodType<'db>::bound_signatures_ 690.12kB 709.09kB +2.75% (18.96kB)
infer_expression_types_impl 55.81MB 55.83MB +0.02% (13.04kB)
check_file_impl 17.82MB 17.81MB -0.07% (12.13kB)
when_constraint_set_assignable_to_owned_impl::interned_arguments 562.72kB 572.09kB +1.66% (9.37kB)
TupleType 752.14kB 760.91kB +1.17% (8.77kB)
try_call_dunder_get_inner::interned_arguments 1.02MB 1.03MB +0.81% (8.43kB)
infer_definition_types 79.27MB 79.28MB +0.01% (7.45kB)
... 32 more

sphinx

Name Old New Diff Outcome
member_lookup_with_policy_inner::interned_arguments 2.51MB 2.91MB +16.21% (415.89kB)
member_lookup_with_policy_inner 6.88MB 6.95MB +0.99% (69.76kB)
when_constraint_set_assignable_to_owned_impl 1.49MB 1.52MB +1.98% (30.28kB)
Type<'db>::apply_specialization_inner_ 1.49MB 1.52MB +1.68% (25.75kB)
Type<'db>::apply_specialization_inner_::interned_arguments 1.38MB 1.40MB +1.13% (15.94kB)
try_call_dunder_get_inner 415.60kB 429.04kB +3.23% (13.44kB)
BoundMethodType 745.23kB 753.20kB +1.07% (7.97kB)
infer_expression_types_impl 20.03MB 20.04MB +0.03% (6.96kB)
Specialization 1.29MB 1.30MB +0.51% (6.72kB)
BoundMethodType<'db>::bound_signatures_ 618.61kB 624.23kB +0.91% (5.62kB)
try_call_dunder_get_inner::interned_arguments 254.01kB 259.19kB +2.04% (5.18kB)
when_constraint_set_assignable_to_owned_impl::interned_arguments 253.43kB 257.38kB +1.56% (3.95kB)
TupleType 549.50kB 553.11kB +0.66% (3.61kB)
infer_definition_types 20.91MB 20.91MB +0.01% (2.67kB)
BoundMethodType<'db>::into_callable_type_ 209.84kB 211.80kB +0.94% (1.97kB)
... 22 more

trio

Name Old New Diff Outcome
member_lookup_with_policy_inner::interned_arguments 775.73kB 899.30kB +15.93% (123.56kB)
member_lookup_with_policy_inner 1.79MB 1.80MB +0.68% (12.52kB)
when_constraint_set_assignable_to_owned_impl 403.76kB 410.30kB +1.62% (6.54kB)
Type<'db>::apply_specialization_inner_ 557.78kB 562.58kB +0.86% (4.80kB)
try_call_dunder_get_inner 128.86kB 133.49kB +3.59% (4.63kB)
Type<'db>::apply_specialization_inner_::interned_arguments 506.41kB 509.14kB +0.54% (2.73kB)
TupleType 99.14kB 101.64kB +2.52% (2.50kB)
ProtocolInterface 80.87kB 82.93kB +2.55% (2.06kB)
Specialization 470.94kB 472.14kB +0.26% (1.20kB)
BoundMethodType 182.58kB 183.75kB +0.64% (1.17kB)
when_constraint_set_assignable_to_owned_impl::interned_arguments 79.49kB 80.52kB +1.30% (1.03kB)
try_call_dunder_get_inner::interned_arguments 88.66kB 89.58kB +1.03% (936.00B)
infer_expression_types_impl 6.37MB 6.37MB +0.01% (840.00B)
BoundMethodType<'db>::bound_signatures_ 146.02kB 146.65kB +0.44% (652.00B)
is_redundant_with_impl::interned_arguments 224.21kB 224.73kB +0.23% (528.00B)
... 15 more

flake8

Name Old New Diff Outcome
member_lookup_with_policy_inner::interned_arguments 196.32kB 226.76kB +15.50% (30.44kB)
when_constraint_set_assignable_to_owned_impl 160.61kB 161.42kB +0.50% (824.00B)
Type<'db>::apply_specialization_inner_ 181.75kB 182.43kB +0.37% (696.00B)
Type<'db>::apply_specialization_inner_::interned_arguments 170.86kB 171.33kB +0.27% (480.00B)
member_lookup_with_policy_inner 516.74kB 517.01kB +0.05% (272.00B)
BoundMethodType<'db>::bound_signatures_ 50.51kB 50.75kB +0.47% (244.00B)
Specialization 179.80kB 180.02kB +0.12% (224.00B)
infer_scope_types_impl 832.42kB 832.61kB +0.02% (192.00B)
BoundMethodType 54.22kB 54.38kB +0.29% (160.00B)
when_constraint_set_assignable_to_owned_impl::interned_arguments 29.13kB 29.22kB +0.29% (88.00B)
BoundMethodType<'db>::into_callable_type_ 16.73kB 16.82kB +0.49% (84.00B)

@astral-sh-bot

astral-sh-bot Bot commented Jun 4, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 14 6 6
invalid-return-type 3 4 7
invalid-assignment 3 6 3
no-matching-overload 2 5 0
unresolved-attribute 2 2 2
not-iterable 1 1 1
unsupported-operator 3 0 0
unused-type-ignore-comment 0 1 0
Total 28 25 19
Raw diff (72 changes)
Tanjun (https://github.com/FasterSpeeding/Tanjun)
+ tanjun/commands/slash.py:1458:22 error[invalid-assignment] Object of type `Self@CommandInteractionOption` is not assignable to `AutocompleteInteractionOption | None`
+ tanjun/commands/slash.py:1464:38 error[unresolved-attribute] Attribute `name` is not defined on `None` in union `AutocompleteInteractionOption | None`
+ tanjun/commands/slash.py:1466:45 error[unresolved-attribute] Attribute `name` is not defined on `None` in union `AutocompleteInteractionOption | None`

apprise (https://github.com/caronc/apprise)
- apprise/plugins/fortysixelks.py:161:23 error[invalid-assignment] Object of type `list[LiteralString | None]` is not assignable to `Iterable[str] | None`
+ apprise/plugins/fortysixelks.py:161:23 error[invalid-assignment] Object of type `list[str | None]` is not assignable to `Iterable[str] | None`
- apprise/plugins/pushover.py:844:13 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments

archinstall (https://github.com/archlinux/archinstall)
+ archinstall/tui/components.py:742:37 error[invalid-argument-type] Argument is incorrect: Expected `Self@MenuItem`, found `MenuItem & ~AlwaysFalsy`

bokeh (https://github.com/bokeh/bokeh)
- src/bokeh/util/serialization.py:367:17 error[invalid-argument-type] Argument to bound method `MaskedArray.filled` is incorrect: Argument type `MaskedArray[tuple[object, ...], dtype[object]]` does not satisfy upper bound `MaskedArray[_ShapeT_co@MaskedArray, _DTypeT_co@MaskedArray]` of type variable `Self`
+ src/bokeh/util/serialization.py:367:17 error[invalid-argument-type] Argument to bound method `MaskedArray.filled` is incorrect: Argument type `ndarray[tuple[Any, ...], dtype[Any]] & MaskedArray[tuple[object, ...], dtype[object]]` does not satisfy upper bound `MaskedArray[_ShapeT_co@MaskedArray, _DTypeT_co@MaskedArray]` of type variable `Self`
+ src/bokeh/util/serialization.py:367:17 error[invalid-argument-type] Argument to bound method `MaskedArray.filled` is incorrect: Argument type `ndarray[tuple[Any, ...], dtype[floating[Any]]] & MaskedArray[tuple[object, ...], dtype[object]]` does not satisfy upper bound `MaskedArray[_ShapeT_co@MaskedArray, _DTypeT_co@MaskedArray]` of type variable `Self`

core (https://github.com/home-assistant/core)
- homeassistant/components/asuswrt/helpers.py:42:16 error[invalid-return-type] Return type does not match returned value: expected `T@translate_to_legacy`, found `dict[Unknown, object]`
+ homeassistant/components/asuswrt/helpers.py:42:16 error[invalid-return-type] Return type does not match returned value: expected `T@translate_to_legacy`, found `dict[str, Any]`
- homeassistant/components/asuswrt/helpers.py:42:17 error[no-matching-overload] No overload of bound method `dict.get` matches arguments

discord.py (https://github.com/Rapptz/discord.py)
- discord/ext/commands/core.py:921:23 error[invalid-argument-type] Argument to bound method `Cog.cog_before_invoke` is incorrect: Expected `Cog`, found `CogT@Command`
- discord/ext/commands/core.py:941:23 error[invalid-argument-type] Argument to bound method `Cog.cog_after_invoke` is incorrect: Expected `Cog`, found `CogT@Command`
+ discord/ui/container.py:143:24 error[invalid-argument-type] Argument to bound method `Item.copy` is incorrect: Argument type `((Self@_init_children, Interaction[Any], Any, /) -> Coroutine[Any, Any, Any]) & Item[object]` does not satisfy upper bound `Item[V@Item]` of type variable `Self`
+ discord/ui/section.py:190:9 error[invalid-argument-type] Argument to bound method `Item._update_view` is incorrect: Argument type `str & Item[object]` does not satisfy upper bound `Item[V@Item]` of type variable `Self`
+ discord/ui/view.py:264:24 error[invalid-argument-type] Argument to bound method `Item.copy` is incorrect: Argument type `((Any, Interaction[Any], Any, /) -> Coroutine[Any, Any, Any]) & Item[object]` does not satisfy upper bound `Item[V@Item]` of type variable `Self`

django-modern-rest (https://github.com/wemake-services/django-modern-rest)
- dmr/streaming/controller.py:38:16 error[invalid-argument-type] Argument to bound method `StreamingController.to_stream` is incorrect: Argument type `StreamingController[object]` does not satisfy upper bound `StreamingController[_SerializerT_co@StreamingController]` of type variable `Self`
+ dmr/streaming/controller.py:38:16 error[invalid-argument-type] Argument to bound method `StreamingController.to_stream` is incorrect: Argument type `Controller[BaseSerializer] & StreamingController[object]` does not satisfy upper bound `StreamingController[_SerializerT_co@StreamingController]` of type variable `Self`

hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
- src/hydra_zen/third_party/beartype.py:125:9 error[invalid-assignment] Object of type `(bound method _T@validates_with_beartype.__init__() -> None) & (Overload[(o: object, /) -> None, (name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None])` is not assignable to attribute `__init__` on type `_T@validates_with_beartype & type`
+ src/hydra_zen/third_party/beartype.py:125:9 error[invalid-assignment] Object of type `(bound method _T@validates_with_beartype & type.__init__() -> None) & (Overload[(o: object, /) -> None, (name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None])` is not assignable to attribute `__init__` on type `_T@validates_with_beartype & type`

jax (https://github.com/google/jax)
+ jax/_src/numpy/lax_numpy.py:3134:20 error[not-iterable] Object of type `(Sequence[int] & tuple[object, ...]) | Array | ndarray[tuple[Any, ...], dtype[Any]] | ... omitted 11 union elements` may not be iterable

mypy (https://github.com/python/mypy)
- mypy/checkmember.py:1520:12 error[invalid-return-type] Return type does not match returned value: expected `F@bind_self_fast`, found `CallableType`

pandas (https://github.com/pandas-dev/pandas)
+ pandas/core/algorithms.py:586:12 error[no-matching-overload] No overload of bound method `ndarray.any` matches arguments
- pandas/core/arrays/boolean.py:264:12 error[invalid-return-type] Return type does not match returned value: expected `tuple[ndarray[tuple[Any, ...], dtype[Any]], ndarray[tuple[Any, ...], dtype[Any]]]`, found `tuple[(Unknown & ndarray[tuple[object, ...], dtype[object]]) | ndarray[tuple[int], dtype[Any]], Unknown | ndarray[Unknown, dtype[Any]] | None | ndarray[tuple[Any, ...], dtype[numpy.bool[builtins.bool]]] | ndarray[tuple[Any, ...], dtype[Any]]]`
+ pandas/core/arrays/boolean.py:264:12 error[invalid-return-type] Return type does not match returned value: expected `tuple[ndarray[tuple[Any, ...], dtype[Any]], ndarray[tuple[Any, ...], dtype[Any]]]`, found `tuple[(Unknown & ndarray[tuple[object, ...], dtype[object]] & ~BooleanArray) | ndarray[tuple[int], dtype[Any]], Unknown | ndarray[Unknown, dtype[Any]] | None | ndarray[tuple[Any, ...], dtype[numpy.bool[builtins.bool]]] | ndarray[tuple[Any, ...], dtype[Any]]]`
- pandas/core/arrays/datetimes.py:3103:21 error[invalid-assignment] Object of type `Timestamp` is not assignable to `_TimestampNoneT1@_maybe_normalize_endpoints`
- pandas/core/arrays/datetimes.py:3106:19 error[invalid-assignment] Object of type `Timestamp` is not assignable to `_TimestampNoneT2@_maybe_normalize_endpoints`
+ pandas/core/arrays/masked.py:1894:52 error[invalid-argument-type] Argument to bound method `BaseMaskedArray._maybe_mask_result` is incorrect: Expected `ndarray[tuple[Any, ...], dtype[Any]]`, found `ndarray[Unknown, dtype[Any]] | numpy.bool[builtins.bool] | ndarray[tuple[Any, ...], dtype[numpy.bool[builtins.bool]]]`
- pandas/core/computation/pytables.py:190:19 error[invalid-argument-type] Argument to bound method `ndarray.ravel` is incorrect: Argument type `ndarray[tuple[object, ...], dtype[object]]` does not satisfy upper bound `ndarray[_ShapeT_co@ndarray, _DTypeT_co@ndarray]` of type variable `Self`
+ pandas/core/computation/pytables.py:190:19 error[invalid-argument-type] Argument to bound method `ndarray.ravel` is incorrect: Argument type `list[Unknown] & ndarray[tuple[object, ...], dtype[object]]` does not satisfy upper bound `ndarray[_ShapeT_co@ndarray, _DTypeT_co@ndarray]` of type variable `Self`
+ pandas/core/groupby/groupby.py:5784:16 error[invalid-return-type] Return type does not match returned value: expected `NDFrameT@GroupBy`, found `NDFrameT@GroupBy | (Self'return@_constructor & Series)`
- pandas/core/indexes/base.py:5233:13 error[unresolved-attribute] Attribute `flags` is not defined on `ExtensionArray` in union `ExtensionArray | ndarray[tuple[Any, ...], dtype[Any]]`
- pandas/core/internals/blocks.py:2439:9 error[unresolved-attribute] Attribute `flags` is not defined on `ExtensionArray` in union `ExtensionArray | ndarray[tuple[Any, ...], dtype[Any]]`
+ pandas/core/ops/mask_ops.py:73:17 error[unsupported-operator] Unary operator `~` is not supported for object of type `ndarray[tuple[Any, ...], dtype[Any]] | (NAType & ndarray[tuple[object, ...], dtype[object]])`
+ pandas/core/ops/mask_ops.py:78:12 error[invalid-return-type] Return type does not match returned value: expected `tuple[ndarray[tuple[Any, ...], dtype[numpy.bool[builtins.bool]]], ndarray[tuple[Any, ...], dtype[numpy.bool[builtins.bool]]]]`, found `tuple[ndarray[tuple[Any, ...], dtype[Any]] | (NAType & ndarray[tuple[object, ...], dtype[object]]) | Unknown, Unknown | ndarray[tuple[Any, ...], dtype[Any]]]`
- pandas/core/resample.py:3215:12 error[invalid-return-type] Return type does not match returned value: expected `FreqIndexT@_asfreq_compat`, found `PeriodIndex | DatetimeIndex | TimedeltaIndex`
+ pandas/core/resample.py:3215:12 error[invalid-return-type] Return type does not match returned value: expected `FreqIndexT@_asfreq_compat`, found `(FreqIndexT@_asfreq_compat & ExactlySized[Literal[0, False]] & PeriodIndex) | DatetimeIndex | TimedeltaIndex`
+ pandas/core/sorting.py:373:31 error[unsupported-operator] Unary operator `-` is not supported for object of type `(ExtensionArray & ndarray[tuple[object, ...], dtype[object]]) | ndarray[tuple[Any, ...], dtype[Any]] | (Index & ndarray[tuple[object, ...], dtype[object]]) | (Series & ndarray[tuple[object, ...], dtype[object]])`
+ pandas/core/sorting.py:382:27 error[unsupported-operator] Unary operator `~` is not supported for object of type `(ExtensionArray & ndarray[tuple[object, ...], dtype[object]]) | ndarray[tuple[Any, ...], dtype[Any]] | (Index & ndarray[tuple[object, ...], dtype[object]]) | (Series & ndarray[tuple[object, ...], dtype[object]])`

prefect (https://github.com/PrefectHQ/prefect)
- src/prefect/utilities/templating/__init__.py:95:47 error[invalid-argument-type] Argument to function `find_placeholders` is incorrect: Argument type `object` does not satisfy constraints (`str`, `int`, `int | float`, `bool`, `dict[Any, Any]`, `list[Any]`, `None`) of type variable `T`
- src/prefect/utilities/templating/__init__.py:227:29 error[no-matching-overload] No overload of function `apply_values` matches arguments
- src/prefect/utilities/templating/__init__.py:235:17 error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `Unknown & ~<class 'NotSet'>` on object of type `dict[str, Any]`
- src/prefect/utilities/templating/__init__.py:237:17 error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `object` on object of type `dict[str, Any]`
- src/prefect/utilities/templating/__init__.py:426:33 error[no-matching-overload] No overload of bound method `dict.get` matches arguments
- src/prefect/utilities/templating/__init__.py:426:33 error[no-matching-overload] No overload of bound method `dict.get` matches arguments
- src/prefect/utilities/templating/__init__.py:432:17 error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any]` on object of type `dict[str, Any]`
- src/prefect/utilities/templating/__init__.py:432:56 error[invalid-argument-type] Argument to function `_resolve` is incorrect: Expected `T@resolve_block_document_references`, found `object`
- src/prefect/utilities/templating/__init__.py:541:20 error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, T@resolve_variables]`
+ src/prefect/utilities/templating/__init__.py:541:20 error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[Any, T@resolve_variables]`
- src/prefect/utilities/templating/__init__.py:541:41 error[invalid-argument-type] Argument to function `_resolve` is incorrect: Expected `T@resolve_variables`, found `object`

psycopg (https://github.com/psycopg/psycopg)
+ psycopg/psycopg/_copy.py:66:9 error[invalid-assignment] Object of type `(bound method Writer & ~AlwaysFalsy.write(data: bytes | bytearray | memoryview[int]) -> None) | (bound method LibpqWriter.write(data: bytes | bytearray | memoryview[int]) -> None)` is not assignable to attribute `_write` of type `(bound method Writer.write(data: bytes | bytearray | memoryview[int]) -> None) | (bound method LibpqWriter.write(data: bytes | bytearray | memoryview[int]) -> None)`
+ psycopg/psycopg/_copy_async.py:63:9 error[invalid-assignment] Object of type `(bound method AsyncWriter & ~AlwaysFalsy.write(data: bytes | bytearray | memoryview[int]) -> CoroutineType[Any, Any, None]) | (bound method AsyncLibpqWriter.write(data: bytes | bytearray | memoryview[int]) -> CoroutineType[Any, Any, None])` is not assignable to attribute `_write` of type `(bound method AsyncWriter.write(data: bytes | bytearray | memoryview[int]) -> CoroutineType[Any, Any, None]) | (bound method AsyncLibpqWriter.write(data: bytes | bytearray | memoryview[int]) -> CoroutineType[Any, Any, None])`

pytest (https://github.com/pytest-dev/pytest)
- src/_pytest/assertion/util.py:96:12 error[invalid-return-type] Return type does not match returned value: expected `list[str]`, found `list[LiteralString]`
+ src/_pytest/skipping.py:298:25 error[invalid-argument-type] Argument to bound method `AbstractRaises.matches` is incorrect: Argument type `object` does not satisfy upper bound `BaseException` of type variable `BaseExcT_1`
+ src/_pytest/skipping.py:298:25 error[invalid-argument-type] Argument to bound method `AbstractRaises.matches` is incorrect: Argument type `object` does not satisfy upper bound `BaseException` of type variable `BaseExcT_1`

pytest-autoprofile (https://gitlab.com/TTsangSC/pytest-autoprofile)
- src/pytest_autoprofile/importers.py:1026:20 error[invalid-return-type] Return type does not match returned value: expected `tuple[list[str], list[str]]`, found `tuple[list[str], list[LiteralString]]`
+ src/pytest_autoprofile/importers.py:1026:20 error[invalid-return-type] Return type does not match returned value: expected `tuple[list[str], list[str]]`, found `tuple[list[str], list[str] | list[LiteralString]]`

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/metadata/__init__.py:89:24 error[invalid-argument-type] Argument is incorrect: Expected `str`, found `object`
+ src/scikit_build_core/metadata/__init__.py:89:35 error[invalid-argument-type] Argument is incorrect: Expected `str`, found `str | list[str] | dict[str, str]`
- src/scikit_build_core/metadata/__init__.py:89:35 error[invalid-argument-type] Argument is incorrect: Expected `str`, found `object`
- src/scikit_build_core/metadata/__init__.py:107:16 error[invalid-return-type] Return type does not match returned value: expected `T@_process_dynamic_metadata`, found `dict[object, dict[str, str]]`
+ src/scikit_build_core/metadata/__init__.py:107:16 error[invalid-return-type] Return type does not match returned value: expected `T@_process_dynamic_metadata`, found `dict[str, dict[str, str]]`
- src/scikit_build_core/metadata/__init__.py:108:51 error[unresolved-attribute] Object of type `object` has no attribute `items`
+ src/scikit_build_core/metadata/__init__.py:108:51 error[unresolved-attribute] Attribute `items` is not defined on `str`, `list[str]` in union `str | list[str] | dict[str, str]`
- src/scikit_build_core/metadata/__init__.py:117:16 error[invalid-return-type] Return type does not match returned value: expected `T@_process_dynamic_metadata`, found `dict[object, list[str]]`
+ src/scikit_build_core/metadata/__init__.py:117:16 error[invalid-return-type] Return type does not match returned value: expected `T@_process_dynamic_metadata`, found `dict[str, list[str]]`
- src/scikit_build_core/metadata/__init__.py:117:40 error[not-iterable] Object of type `object` is not iterable

spark (https://github.com/apache/spark)
+ python/pyspark/mllib/feature.py:636:20 error[invalid-return-type] Return type does not match returned value: expected `Vector | RDD[Vector]`, found `RDD[Vector | RDD[Vector]]`
+ python/pyspark/mllib/feature.py:636:33 error[invalid-argument-type] Argument to bound method `RDD.map` is incorrect: Expected `(object, /) -> Vector | RDD[Vector]`, found `Overload[(document: Iterable[Hashable]) -> Vector, (document: RDD[Iterable[Hashable]]) -> RDD[Vector]]`
- python/pyspark/pandas/base.py:1065:16 error[unresolved-attribute] Object of type `IndexOpsMixin` has no attribute `rename`
+ python/pyspark/pandas/base.py:1065:16 error[unresolved-attribute] Object of type `IndexOpsLike@notnull` has no attribute `rename`
- python/pyspark/sql/session.py:1639:62 error[invalid-argument-type] Argument to bound method `ndarray.squeeze` is incorrect: Argument type `ndarray[tuple[object, ...], dtype[object]]` does not satisfy upper bound `ndarray[_ShapeT_co@ndarray, _DTypeT_co@ndarray]` of type variable `Self`
+ python/pyspark/sql/session.py:1639:62 error[invalid-argument-type] Argument to bound method `ndarray.squeeze` is incorrect: Argument type `ndarray[tuple[object, ...], dtype[object]] & ~DataFrame` does not satisfy upper bound `ndarray[_ShapeT_co@ndarray, _DTypeT_co@ndarray]` of type variable `Self`

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/container_util.py:186:22 error[invalid-assignment] Object of type `Top[defaultdict[Unknown, Unknown]]` is not assignable to `None | Iterable[str | dtype[Any] | type | None] | dtype[Any] | type | dict[Hashable, str | dtype[Any] | type | None]`
- static_frame/core/util.py:451:22 error[not-iterable] Object of type `(int & ndarray[tuple[object, ...], dtype[object]]) | (list[int] & ndarray[tuple[object, ...], dtype[object]]) | (ndarray[Any, dtype[signedinteger[_64Bit]]] & ndarray[tuple[object, ...], dtype[object]])` is not iterable
+ static_frame/core/util.py:451:22 error[not-iterable] Object of type `(int & ndarray[tuple[object, ...], dtype[object]]) | (list[int] & ndarray[tuple[object, ...], dtype[object]]) | (ndarray[Any, dtype[signedinteger[_64Bit]]] & ndarray[tuple[object, ...], dtype[object]])` may not be iterable

sympy (https://github.com/sympy/sympy)
- sympy/polys/polyclasses.py:1308:31 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ sympy/polys/rings.py:884:20 error[invalid-argument-type] Argument to bound method `PolyElement._try_add_ground` is incorrect: Argument type `PolyElement[PolyElement[Er@PolyElement]] & ~PolyElement[Er@PolyElement]` does not satisfy upper bound `PolyElement[Er@PolyElement]` of type variable `Self`
+ sympy/polys/rings.py:944:20 error[invalid-argument-type] Argument to bound method `PolyElement._try_rsub_ground` is incorrect: Argument type `PolyElement[PolyElement[Er@PolyElement]] & ~PolyElement[Er@PolyElement]` does not satisfy upper bound `PolyElement[Er@PolyElement]` of type variable `Self`
+ sympy/polys/rings.py:1021:20 error[invalid-argument-type] Argument to bound method `PolyElement._try_mul_ground` is incorrect: Argument type `PolyElement[PolyElement[Er@PolyElement]] & ~AlwaysFalsy & ~PolyElement[Er@PolyElement]` does not satisfy upper bound `PolyElement[Er@PolyElement]` of type variable `Self`

vision (https://github.com/pytorch/vision)
+ torchvision/transforms/functional.py:154:32 error[no-matching-overload] No overload of bound method `ndarray.transpose` matches arguments
+ torchvision/transforms/functional.py:154:46 error[invalid-argument-type] Argument to bound method `Image.transpose` is incorrect: Expected `Transpose`, found `tuple[Literal[2], Literal[0], Literal[1]]`

xarray (https://github.com/pydata/xarray)
- xarray/computation/rolling.py:1216:20 error[invalid-return-type] Return type does not match returned value: expected `T_Xarray@Coarsen`, found `DataArray`
- xarray/core/groupby.py:781:20 error[invalid-return-type] Return type does not match returned value: expected `T_Xarray@GroupBy`, found `DataArray`
- xarray/core/indexes.py:1929:23 error[invalid-assignment] Object of type `Index` is not assignable to `T_PandasOrXarrayIndex@Indexes`
+ xarray/core/indexes.py:1929:23 error[invalid-assignment] Object of type `PandasIndex | (T_PandasOrXarrayIndex@Indexes & xarray.core.indexes.Index & ~Top[pandas.core.indexes.base.Index[Any]])` is not assignable to `T_PandasOrXarrayIndex@Indexes`
- xarray/namedarray/core.py:1000:54 error[invalid-argument-type] Argument to bound method `_sparsearrayfunction.todense` is incorrect: Argument type `_sparsearrayfunction[object, object]` does not satisfy upper bound `_sparsearrayfunction[_ShapeType_co@_sparsearrayfunction, _DType_co@_sparsearrayfunction]` of type variable `Self`
+ xarray/namedarray/core.py:1000:54 error[invalid-argument-type] Argument to bound method `_sparsearrayfunction.todense` is incorrect: Argument type `_arrayapi[Any, _DType_co@NamedArray] & _sparsearrayfunction[object, object]` does not satisfy upper bound `_sparsearrayfunction[_ShapeType_co@_sparsearrayfunction, _DType_co@_sparsearrayfunction]` of type variable `Self`
+ xarray/namedarray/core.py:1000:54 error[invalid-argument-type] Argument to bound method `_sparsearrayfunction.todense` is incorrect: Argument type `_arrayfunction[Any, _DType_co@NamedArray] & _sparsearrayfunction[object, object]` does not satisfy upper bound `_sparsearrayfunction[_ShapeType_co@_sparsearrayfunction, _DType_co@_sparsearrayfunction]` of type variable `Self`

Full report with detailed diff (timing results)

@charliermarsh charliermarsh marked this pull request as ready for review June 4, 2026 13:55
@astral-sh-bot astral-sh-bot Bot requested a review from oconnor663 June 4, 2026 13:55
@charliermarsh charliermarsh force-pushed the charlie/preserve-intersection-receivers branch from 6642459 to 4289d95 Compare June 4, 2026 14:04
@AlexWaygood

Copy link
Copy Markdown
Member

I used the "summarise ecosystem results" skill and this is what Codex came up with, in case it's helpful

Details

PR #25626 ecosystem summary

Result

The report contains 72 changes across 21 projects:

Change Count
Added 28
Removed 25
Changed 19

The changes are mechanically coherent with the PR. They reduce to preserving
the full intersection as the receiver when binding descriptors and Self,
instead of binding against only the positive element where the member was
found.

The ecosystem impact splits into two broad groups:

  1. Precision improvements. Constrained TypeVar values keep useful key,bvalue, and return types; narrowed methods returning Self preserve the original type variable; several overload, attribute, assignment, and return errors disappear.
  2. Follow-up limitations exposed by the more precise receiver. Generic Self upper-bound checks reject intersections that contain a valid nominal element, truthiness constraints leak into bound-method types, overlapping union members can both contribute methods, and one generic bound-method call emits duplicate diagnostics.

The largest actionable cluster is the second group. In particular, the new Self-upper-bound diagnostics in Bokeh, discord.py, django-modern-rest, pandas, Spark, SymPy, and xarray all have the same shape: the receiver now correctly retains an intersection, but generic Self validation does not accept that intersection as satisfying the nominal upper bound.

Minimized changes

1. Self attribute binding can widen an assignment

This is the Tanjun pattern. Accessing a list[Self] attribute on a truthiness-narrowed subclass now binds Self using the full receiver.

from typing import Self


class Option:
    name: str
    options: list[Self]

    def __bool__(self) -> bool:
        return True


class AutocompleteOption(Option):
    pass


def select(
    option: AutocompleteOption | None,
    top_level: list[AutocompleteOption],
) -> None:
    if not option and top_level:
        option = top_level[0]
    elif option and option.options:
        # Base: no diagnostic.
        # PR: error[invalid-assignment] Object of type `Self@Option` is not
        #     assignable to `AutocompleteOption | None`
        option = option.options[0]
    else:
        return
    # PR consequence:
    # error[unresolved-attribute] Attribute `name` is not defined on `None`
    # in union `AutocompleteOption | None`
    print(option.name)

Original entries: [Tanjun assignment][tanjun-assignment] and the two resulting [name accesses][tanjun-name].

2. Constrained container narrowing becomes more precise

This family explains most of the beneficial Prefect, Home Assistant, and scikit-build-core changes.

from typing import Any, TypeVar

MAPPING: dict[str, str] = {}
T = TypeVar("T", dict[str, Any], list[Any])


def translate(value: T) -> T:
    if isinstance(value, dict):
        # Base:
        # error[invalid-return-type] Return type does not match returned value:
        # expected `T@translate`, found `dict[Unknown, object]`
        # error[no-matching-overload] No overload of bound method `dict.get`
        # matches arguments
        #
        # PR:
        # error[invalid-return-type] Return type does not match returned value:
        # expected `T@translate`, found `dict[str, Any]`
        return {MAPPING.get(key, key): item for key, item in value.items()}
    return value

The same receiver-preservation mechanism narrows dictionary keys and values instead of falling back to object or Unknown. It covers:

  • [Home Assistant's translate_to_legacy][core-container].
  • [Prefect's templating recursion][prefect-container].
  • [scikit-build-core's dynamic metadata processing][scikit-container].
  • [apprise's MutableMapping.update call][apprise-update].
  • [static-frame's copied defaultdict][static-defaultdict].

3. A narrowed Self return preserves the caller's type variable

from typing import Self, TypeVar


class Base:
    pass


class Derived(Base):
    def copy(self) -> Self:
        return self


T = TypeVar("T", bound=Base)


def copy_if_derived(value: T) -> T:
    if isinstance(value, Derived):
        # Base:
        # error[invalid-return-type] Return type does not match returned value:
        # expected `T@copy_if_derived`, found `Derived`
        #
        # PR: no diagnostic.
        return value.copy()
    return value

This explains removed diagnostics in [mypy][mypy-self], [pandas endpoint normalization][pandas-self], [discord.py Cog hooks][discord-self], and [xarray rolling/groupby][xarray-self].

4. Generic Self validation sees the full intersection

NumPy's stubs make the upper-bound problem particularly clear:

from typing import Any

import numpy as np
import numpy.typing as npt


def fill(array: npt.ANDArray[Any]) -> None:
    if isinstance(array, np.ma.MaskedArray):
        # Base:
        # error[invalid-argument-type] Argument to bound method
        # `MaskedArray.filled` is incorrect: Argument type
        # `MaskedArray[tuple[object, ...], dtype[object]]` does not satisfy
        # upper bound `MaskedArray[_ShapeT_co@MaskedArray,
        # _DTypeT_co@MaskedArray]` of type variable `Self`
        #
        # PR:
        # error[invalid-argument-type] Argument to bound method
        # `MaskedArray.filled` is incorrect: Argument type
        # `ndarray[tuple[Any, ...], dtype[Any]] &
        # MaskedArray[tuple[object, ...], dtype[object]]` does not satisfy
        # upper bound `MaskedArray[_ShapeT_co@MaskedArray,
        # _DTypeT_co@MaskedArray]` of type variable `Self`
        array.filled(np.nan)

The more precise receiver is visible and expected. The failure to satisfy the nominal Self upper bound is the common follow-up issue. This family covers [Bokeh][bokeh-self], [discord.py UI items][discord-ui-self], [django-modern-rest][django-self], [pandas ravel][pandas-ravel], [Spark squeeze][spark-squeeze], [SymPy's PolyElement methods][sympy-self], and [xarray sparse arrays][xarray-sparse].

5. Truthiness constraints enter Self-typed callbacks

from collections.abc import Callable
from typing import Self


class Item:
    preview: Callable[[Self], None] | None

    def __bool__(self) -> bool:
        return True


def render(item: Item | None) -> None:
    if not item:
        return
    if item.preview is not None:
        # Base: no diagnostic.
        # PR: error[invalid-argument-type] Argument is incorrect:
        # Expected `Self@Item`, found `Item & ~AlwaysFalsy`
        item.preview(item)

This is the minimized [archinstall diagnostic][archinstall-truthiness]. The same preserved ~AlwaysFalsy component appears in the two [psycopg bound-method assignments][psycopg-truthiness] and one of the [SymPy receiver errors][sympy-truthiness].

6. Type-object descriptors bind to the intersection

from typing import TypeVar

T = TypeVar("T")


def decorate(value: T) -> T:
    if isinstance(value, type):
        # Base:
        # error[invalid-assignment] Object of type
        # `(bound method T@decorate.__init__() -> None) & (Overload[...])`
        # is not assignable to attribute `__init__` on type
        # `T@decorate & type`
        #
        # PR:
        # error[invalid-assignment] Object of type
        # `(bound method T@decorate & type.__init__() -> None) &
        # (Overload[...])` is not assignable to attribute `__init__` on type
        # `T@decorate & type`
        value.__init__ = value.__init__
    return value

This exactly isolates the changed receiver in [hydra-zen's class decorator][hydra-class].

7. Literal-string methods retain different receiver information

The direct form changes the inferred return:

def split_lines(explanation: str) -> list[str]:
    # Base:
    # error[invalid-return-type] Return type does not match returned value:
    # expected `list[str]`, found `list[LiteralString]`
    #
    # PR:
    # error[invalid-return-type] Return type does not match returned value:
    # expected `list[str]`, found `list[str] | list[LiteralString]`
    return (explanation or "").split("\n")

That matches [pytest-autoprofile][autoprofile-literal]. When the result is copied through a loop, the old list[LiteralString] error disappears entirely:

def split_lines(explanation: str) -> list[str]:
    raw_lines = (explanation or "").split("\n")
    lines = [raw_lines[0]]
    for value in raw_lines[1:]:
        lines.append(value)
    # Base:
    # error[invalid-return-type] Return type does not match returned value:
    # expected `list[str]`, found `list[LiteralString]`
    #
    # PR: no diagnostic.
    return lines

That matches [pytest][pytest-literal]. The changed [apprise list element type][apprise-literal] is the same literal-string receiver family.

8. Generic operators retain overlapping intersection members

from typing import Generic, Self, TypeVar

T = TypeVar("T")


class Array(Generic[T]):
    def __invert__(self) -> Self:
        return self


class Missing:
    pass


def invert(value: Array[int] | Missing) -> Array[int]:
    if isinstance(value, Array):
        # Base: no diagnostic.
        # PR: error[invalid-return-type] Return type does not match returned
        # value: expected `Array[int]`, found
        # `Array[int] | (Missing & Top[Array[Unknown]])`
        return ~value
    return Array()

This is the small, import-free version of the new pandas operator and return diagnostics, including [~ in mask_ops][pandas-mask], [- and ~ in sorting][pandas-sorting], and the richer return intersections in [resampling][pandas-resample] and [BooleanArray conversion][pandas-boolean].

9. Recursive overload binding changes the callback type

from __future__ import annotations

from collections.abc import Callable, Iterable
from typing import Generic, TypeVar, overload

T_co = TypeVar("T_co", covariant=True)
T = TypeVar("T")
U = TypeVar("U")


class RDD(Generic[T_co]):
    def map(self: RDD[T], function: Callable[[T], U]) -> RDD[U]:
        return RDD()


class Transformer:
    @overload
    def transform(self, value: Iterable[object]) -> int: ...

    @overload
    def transform(self, value: RDD[Iterable[object]]) -> RDD[int]: ...

    def transform(
        self, value: Iterable[object] | RDD[Iterable[object]]
    ) -> int | RDD[int]:
        if isinstance(value, RDD):
            # Base: no diagnostic.
            # PR:
            # error[invalid-return-type] Return type does not match returned
            # value: expected `int | RDD[int]`, found `RDD[int | RDD[int]]`
            # error[invalid-argument-type] Argument to bound method `RDD.map`
            # is incorrect: Expected `(object, /) -> int | RDD[int]`, found
            # `Overload[(value: Iterable[object]) -> int,
            # (value: RDD[Iterable[object]]) -> RDD[int]]`
            return value.map(self.transform)
        return 0

This reproduces both additions in [Spark's recursive transformer][spark-recursive].

10. Complex array-like intersections become maybe-iterable

JAX requires its real ArrayLike union to retain the relevant overlap:

from collections.abc import Sequence

import numpy as np
from jax._src.typing import Array, ArrayLike


def split(indices_or_sections: int | Sequence[int] | ArrayLike) -> None:
    if (
        isinstance(indices_or_sections, (tuple, list))
        or isinstance(indices_or_sections, (np.ndarray, Array))
        and indices_or_sections.ndim > 0
    ):
        # Base: no diagnostic.
        # PR: error[not-iterable] Object of type
        # `(Sequence[int] & tuple[object, ...]) | Array |
        # ndarray[tuple[Any, ...], dtype[Any]] | ... omitted 11 union elements`
        # may not be iterable
        for index in indices_or_sections:
            print(index)

This reproduces the [JAX addition][jax-iterable]. The same more-conservative iterability classification changes static-frame's wording from "is not iterable" to "may not be iterable" for its [exact-ndarray branch][static-iterable].

11. Overlapping members both contribute transpose

import numpy as np
from PIL import Image


def transpose(pic: Image.Image | np.ndarray) -> None:
    if isinstance(pic, np.ndarray):
        # Base: no diagnostic.
        # PR:
        # error[no-matching-overload] No overload of bound method
        # `ndarray.transpose` matches arguments
        # error[invalid-argument-type] Argument to bound method
        # `Image.transpose` is incorrect: Expected `Transpose`, found
        # `tuple[Literal[2], Literal[0], Literal[1]]`
        pic.transpose((2, 0, 1))

This exactly reproduces both [TorchVision additions][vision-transpose]. The second diagnostic demonstrates that preserving the overlap also retains PIL.Image as a possible positive element after the np.ndarray check.

12. A generic bound method can emit duplicate diagnostics

from typing import Generic, TypeGuard, TypeVar

E = TypeVar("E", bound=BaseException)
T = TypeVar("T", bound=BaseException)


class Matcher(Generic[E]):
    def matches(self: "Matcher[T]", exception: BaseException) -> TypeGuard[T]:
        return True


def check(
    matcher: type[BaseException]
    | tuple[type[BaseException], ...]
    | Matcher[BaseException],
    value: object,
) -> None:
    if isinstance(matcher, Matcher):
        # Base: one diagnostic:
        # error[invalid-argument-type] Argument to bound method
        # `Matcher.matches` is incorrect: Expected `BaseException`,
        # found `object`
        #
        # PR: the same diagnostic is emitted three times.
        matcher.matches(value)

The ecosystem diff therefore contains two added copies at the same [pytest call site][pytest-duplicates]. This is a diagnostic de-duplication regression, not a newly discovered second or third type error.

Verification

Fresh binaries were built from the exact report revisions:

$ git checkout --detach 7c6dcd9f2611999c449143d241c582dedf287964
$ CARGO_PROFILE_PROFILING_DEBUG=line-tables-only \
    cargo build --package ty --profile profiling

$ git checkout --detach a9cdb395ba7ffa35a5c0dbf63004231afb3166be
$ CARGO_PROFILE_PROFILING_DEBUG=line-tables-only \
    cargo build --package ty --profile profiling

Every project was prepared at the permalinked revision below using
mypy_primer commit 23bbdd55fea37ca2489043d1327dbe35c4fc7083
and:

$ --exclude-newer 2026-06-04T14:05:44Z

Each project-specific ty check command was then run once with ty-base and
once with ty-pr under the report's ty-ecosystem.toml. The retained
standalone reducers used:

$ TY_CONFIG_FILE=/private/tmp/pr25626-artifacts-current/ty-ecosystem.toml \
    /private/tmp/pr25626-artifacts-current/ty-base check repro.py \
    --python-version 3.11 --output-format concise
$ TY_CONFIG_FILE=/private/tmp/pr25626-artifacts-current/ty-ecosystem.toml \
    /private/tmp/pr25626-artifacts-current/ty-pr check repro.py \
    --python-version 3.11 --output-format concise

The NumPy, JAX, and Pillow reducers additionally used the corresponding
project virtual environment via --python <venv>.

The final audit result was:

verified 91 diagnostic sides from 72 entries

The macOS reproductions produced a few additional NumPy-stub duplicates in
pandas and static-frame that are absent from the Linux report. They were not
counted as report entries; all 91 expected sides were still present.

@charliermarsh

Copy link
Copy Markdown
Member Author

Thanks! I had Codex do a similar analysis and concluded that these were mostly existing limitations made visible by our change. Is that your impression, at first glance? Or would you expect them to be fixed here?

Tragically, I have a stack of seven additional commits / PRs that go on to improve the behavior here, with titles like...

  1. Correct Self binding for intersection refinements
  2. Preserve Self for class-object intersection receivers
  3. Preserve constrained intersection member alternatives
  4. Preserve Self through decorated callables
  5. Bind Self in generic callable defaults
  6. Recover Self from returned callables
  7. Preserve intersection member alternatives during fallback

@AlexWaygood

Copy link
Copy Markdown
Member

I haven't actually looked through the analysis yet I'm afraid 😄 just set it going on it earlier and figured I'd post it in case it's useful!

@charliermarsh charliermarsh force-pushed the charlie/preserve-intersection-receivers branch 4 times, most recently from 57c3a5f to 4c2ffb3 Compare June 4, 2026 23:51
Comment thread crates/ty_python_semantic/resources/mdtest/call/functools_partial.md Outdated
Comment thread crates/ty_python_semantic/resources/mdtest/call/functools_partial.md Outdated
@charliermarsh charliermarsh force-pushed the charlie/preserve-intersection-receivers branch 2 times, most recently from 9834320 to 8b4853d Compare June 5, 2026 18:41
Comment thread crates/ty_python_semantic/resources/mdtest/call/methods.md Outdated
Comment thread crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md Outdated
@oconnor663 oconnor663 removed their assignment Jun 5, 2026
@charliermarsh charliermarsh force-pushed the charlie/preserve-intersection-receivers branch from 8b4853d to 98bf422 Compare June 6, 2026 01:05
@charliermarsh charliermarsh enabled auto-merge (squash) June 6, 2026 01:05
@charliermarsh charliermarsh merged commit 0569f2c into main Jun 6, 2026
58 checks passed
@charliermarsh charliermarsh deleted the charlie/preserve-intersection-receivers branch June 6, 2026 01:09

@sharkdp sharkdp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry for the post-merge review, but I wanted to understand this first before reviewing #25704.

Comment on lines +1866 to +1867
For `Intersection[A, B]`, member lookup searches `A` and `B` separately to find the attribute. Once
found, however, descriptors and `Self` must be bound using the full `A & B` receiver.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: would be nice to move this to the section(s) below.

Comment on lines +1888 to +1907
from typing import Any, TypeVar, overload
from typing_extensions import Self
from ty_extensions import Intersection

T = TypeVar("T")

class Descriptor:
@overload
def __get__(self, instance: None, owner: type, /) -> Self: ...
@overload
def __get__(self, instance: T, owner: type | None = None, /) -> T: ...
def __get__(self, instance: object, owner: type | None = None, /) -> Any: ...

class A:
desc = Descriptor()

class B: ...

def _(a_and_b: Intersection[A, B]):
reveal_type(a_and_b.desc) # revealed: A & B

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: The instance: None overload is irrelevant/distracting here?

Suggested change
from typing import Any, TypeVar, overload
from typing_extensions import Self
from ty_extensions import Intersection
T = TypeVar("T")
class Descriptor:
@overload
def __get__(self, instance: None, owner: type, /) -> Self: ...
@overload
def __get__(self, instance: T, owner: type | None = None, /) -> T: ...
def __get__(self, instance: object, owner: type | None = None, /) -> Any: ...
class A:
desc = Descriptor()
class B: ...
def _(a_and_b: Intersection[A, B]):
reveal_type(a_and_b.desc) # revealed: A & B
from typing import TypeVar
from typing_extensions import Self
from ty_extensions import Intersection
T = TypeVar(name="T")
class Descriptor:
def __get__(self, instance: T, owner: type | None = None, /) -> T:
return instance
class A:
desc = Descriptor()
class B: ...
def _(a_and_b: Intersection[A, B]):
reveal_type(a_and_b.desc) # revealed: A & B


def test(x: Intersection[X, Y]) -> None:
reveal_type(x.f) # revealed: (bound method X.f() -> int) & (bound method Y.f() -> int)
reveal_type(x.f) # revealed: bound method X & Y.f() -> int

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These display types are very unfortunate. Can we make this (X & Y).f() -> int?

Comment on lines +104 to +105
the underlying function object. A protocol refinement of the bound method must not be used as the
receiver for that underlying-function fallback:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't understand this. Why should we make an exception here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

method = C().f is a bound method. When you evaluate method.__globals__, if it doesn't exist on method, then CPython tries method.__func__.__globals__. When we narrow with a protocol, only the bound method gets refined, and not the fallback __func__ -- if we apply the intersection to method.__func__ too, then method.__globals__ becomes Unknown.

I don't know if it's really an exception, IIUC lookup is being done on a different object so we need to respect the receivers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lookup is being done on a different object so we need to respect the receivers.

Thank you, that makes sense to me!

Comment thread crates/ty_python_semantic/resources/mdtest/call/methods.md
Comment on lines +1576 to +1578
Standard `partial` attributes like `.func`, `.args`, and `.keywords` should be accessible. Runtime
protocol narrowing can add a refinement to the concrete `partial` object, but attributes provided by
`partial` must remain precise when looked up through that intersection.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question here: what makes partial special? Why should it be excluded from the behavior implemented in this PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think you're right that this isn't special, I think it was leftover from a prior workaround during a refactor, my bad.

Comment thread crates/ty_python_semantic/src/types.rs
Comment thread crates/ty_python_semantic/src/types.rs
@charliermarsh

Copy link
Copy Markdown
Member Author

Fixed in #25819, thank you.

charliermarsh added a commit that referenced this pull request Jun 10, 2026
## Summary

This follows up on [the post-merge
review](#25626 (review))
of #25626.

`functools.partial` now preserves the full intersection receiver when
delegating member lookup to its nominal `partial[T]` view. Bound-method
fallback still resets the receiver when lookup switches to the separate
underlying function object; the implementation and regression coverage
now make that distinction explicit.

This also parenthesizes compound bound-method receivers in displayed
types and clarifies and relocates the related intersection receiver
documentation and tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

4 participants