Skip to content

[ty] Add disjointness for protocol method members#25315

Merged
charliermarsh merged 1 commit into
mainfrom
charlie/non-never
May 30, 2026
Merged

[ty] Add disjointness for protocol method members#25315
charliermarsh merged 1 commit into
mainfrom
charlie/non-never

Conversation

@charliermarsh
Copy link
Copy Markdown
Member

@charliermarsh charliermarsh commented May 22, 2026

Summary

This PR implements disjointness checking for protocols with method members by comparing non-Never return types, e.g., we now understand that a fixed-length tuple whose synthesized __len__ returns Literal[3] is disjoint from a protocol requiring __len__ to return Literal[2]:

from typing import Literal, Protocol
from ty_extensions import is_disjoint_from, static_assert

class HasLengthTwo(Protocol):
    def __len__(self) -> Literal[2]: ...

static_assert(is_disjoint_from(tuple[int, int, int], HasLengthTwo))

This is a prerequisite for using synthesized exact-length protocols to narrow unions of fixed-length tuples and sequence patterns (see: #22134 (comment)).

Per that comment, this isn't fully sound: a subclass can override an otherwise incompatible method with a Never-returning implementation and therefore inhabit both types that we infer to be disjoint.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label May 22, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 22, 2026

Typing conformance results

No changes detected ✅

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

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 22, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 709.52MB 709.57MB +0.01% (51.46kB)
trio 109.13MB 109.15MB +0.01% (16.05kB)
sphinx 261.06MB 261.07MB +0.00% (10.00kB)
flake8 43.95MB 43.95MB +0.00% (1.28kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
BoundMethodType<'db>::bound_signatures_ 664.14kB 671.35kB +1.09% (7.21kB)
infer_expression_types_impl 60.29MB 60.29MB +0.01% (7.15kB)
is_redundant_with_impl 2.15MB 2.16MB +0.28% (6.14kB)
infer_definition_types 88.41MB 88.41MB +0.01% (5.77kB)
is_redundant_with_impl::interned_arguments 2.36MB 2.36MB +0.19% (4.47kB)
FunctionType<'db>::signature_ 4.53MB 4.53MB +0.09% (4.26kB)
CallableType 2.78MB 2.78MB +0.13% (3.70kB)
infer_scope_types_impl 50.63MB 50.63MB +0.00% (2.29kB)
BoundMethodType<'db>::into_callable_type_ 219.76kB 221.89kB +0.97% (2.13kB)
UnionType 1.32MB 1.32MB +0.10% (1.38kB)
StaticClassLiteral<'db>::try_mro_ 5.34MB 5.34MB +0.03% (1.37kB)
Type<'db>::member_lookup_with_policy_ 17.13MB 17.13MB +0.01% (1.20kB)
infer_deferred_types 10.39MB 10.40MB +0.01% (776.00B)
infer_expression_type_impl 8.22MB 8.22MB +0.01% (540.00B)
Type<'db>::try_call_dunder_get_ 11.52MB 11.52MB +0.00% (532.00B)
... 22 more

trio

Name Old New Diff Outcome
BoundMethodType<'db>::bound_signatures_ 141.93kB 144.98kB +2.15% (3.05kB)
FunctionType<'db>::signature_ 1.12MB 1.12MB +0.26% (2.96kB)
CallableType 692.96kB 695.35kB +0.34% (2.39kB)
infer_expression_types_impl 6.74MB 6.74MB +0.02% (1.63kB)
BoundMethodType<'db>::into_callable_type_ 51.35kB 52.50kB +2.24% (1.15kB)
infer_deferred_types 2.11MB 2.11MB +0.04% (924.00B)
Type<'db>::member_lookup_with_policy_ 1.93MB 1.94MB +0.03% (700.00B)
Type<'db>::class_member_with_policy_ 2.00MB 2.00MB +0.02% (520.00B)
Type<'db>::try_call_dunder_get_ 1.31MB 1.31MB +0.04% (508.00B)
infer_definition_types 7.40MB 7.40MB +0.01% (420.00B)
Type<'db>::class_member_with_policy_::interned_arguments 1.09MB 1.09MB +0.04% (416.00B)
FunctionType 1.57MB 1.57MB +0.02% (384.00B)
infer_scope_types_impl 4.12MB 4.12MB +0.01% (300.00B)
infer_expression_type_impl 1.36MB 1.36MB +0.02% (216.00B)
Type<'db>::member_lookup_with_policy_::interned_arguments 897.71kB 897.81kB +0.01% (104.00B)
... 9 more

sphinx

Name Old New Diff Outcome
BoundMethodType<'db>::bound_signatures_ 619.52kB 621.03kB +0.24% (1.52kB)
infer_expression_types_impl 21.93MB 21.93MB +0.01% (1.45kB)
infer_definition_types 23.39MB 23.39MB +0.00% (936.00B)
CallableType 1.36MB 1.36MB +0.06% (880.00B)
Type<'db>::member_lookup_with_policy_ 7.28MB 7.28MB +0.01% (820.00B)
FunctionType<'db>::signature_ 2.60MB 2.60MB +0.03% (704.00B)
BoundMethodType<'db>::into_callable_type_ 210.57kB 211.23kB +0.31% (672.00B)
Type<'db>::class_member_with_policy_ 7.94MB 7.94MB +0.01% (592.00B)
Type<'db>::try_call_dunder_get_ 5.11MB 5.11MB +0.01% (568.00B)
code_generator_of_static_class 826.22kB 826.68kB +0.06% (472.00B)
when_constraint_set_assignable_to_owned_impl 2.41MB 2.41MB -0.02% (456.00B)
infer_scope_types_impl 13.42MB 13.42MB +0.00% (444.00B)
Type<'db>::class_member_with_policy_::interned_arguments 4.21MB 4.21MB +0.01% (416.00B)
Type<'db>::apply_specialization_ 1.74MB 1.74MB +0.02% (400.00B)
FunctionType 3.56MB 3.56MB +0.01% (384.00B)
... 18 more

flake8

Name Old New Diff Outcome
infer_deferred_types 512.80kB 513.11kB +0.06% (320.00B)
FunctionType<'db>::signature_ 403.04kB 403.34kB +0.08% (316.00B)
BoundMethodType<'db>::bound_signatures_ 50.36kB 50.66kB +0.59% (304.00B)
BoundMethodType<'db>::into_callable_type_ 16.65kB 16.82kB +0.99% (168.00B)
CallableType 205.67kB 205.81kB +0.07% (144.00B)
infer_scope_types_impl 868.14kB 868.18kB +0.00% (36.00B)
infer_expression_types_impl 1.09MB 1.09MB +0.00% (24.00B)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 22, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 0 2 0
Total 0 2 0

Raw diff:

scikit-learn (https://github.com/scikit-learn/scikit-learn)
- sklearn/externals/array_api_extra/_lib/_funcs.py:132:44 error[invalid-argument-type] Argument to function `array_namespace` is incorrect: Expected `Array | int | float | complex | None`, found `object`

scipy (https://github.com/scipy/scipy)
- subprojects/array_api_extra/src/array_api_extra/_lib/_funcs.py:148:44 error[invalid-argument-type] Argument to function `array_namespace` is incorrect: Expected `Array | int | float | complex | None`, found `object`

Full report with detailed diff (timing results)

@charliermarsh charliermarsh changed the title [ty] View ecosystem results for non-never disjointness [ty] Add disjointness for protocol method members May 22, 2026
@charliermarsh charliermarsh force-pushed the charlie/non-never branch 2 times, most recently from 86d8a90 to 617e16c Compare May 22, 2026 12:53
@charliermarsh charliermarsh force-pushed the charlie/non-never branch 2 times, most recently from 94a4ba2 to 43fb9d3 Compare May 22, 2026 13:36
@charliermarsh charliermarsh marked this pull request as ready for review May 22, 2026 14:45
@charliermarsh charliermarsh force-pushed the charlie/non-never branch 4 times, most recently from d9f1196 to 3921ec4 Compare May 28, 2026 10:36
Comment thread crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md Outdated
@charliermarsh charliermarsh force-pushed the charlie/non-never branch 3 times, most recently from 58247e2 to 7bd614b Compare May 30, 2026 09:05
@charliermarsh charliermarsh force-pushed the charlie/non-never branch 2 times, most recently from 9a39019 to 248cd6c Compare May 30, 2026 14:07
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 30, 2026

Merging this PR will not alter performance

✅ 65 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing charlie/non-never (a30f5b9) with main (6d2ca1a)

Open in CodSpeed

Footnotes

  1. 60 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Nice! Have you checked what the reason is for the ecosystem hits going away?

When I suggested this, I was thinking we'd do this in Callable <-> Callable disjointness rather than Protocol <-> <other type> disjointness. In general, we want disjointness for Protocols with method members to be consistent with disjointness for Callables -- protocols with method members generally just delegate to type relations on Callables, broadly speaking. Making this patch more general would broaden the unsoundness, though, so maybe it's better to go for this limited patch now and experiment with generalising it as a followup

static_assert(is_subtype_of(NeverLengthSubclass, LengthThree))
static_assert(is_subtype_of(NeverLengthSubclass, HasLengthTwo))

# Intentionally unsound: `NeverLengthSubclass` inhabits both operands.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# Intentionally unsound: `NeverLengthSubclass` inhabits both operands.
# Intentionally unsound: `NeverLengthSubclass` inhabits both operands,
# but pragmatically, nobody is ever likely to write such a class

@charliermarsh
Copy link
Copy Markdown
Member Author

It looks like it's because we now prove that Array and tuple[object, ...] are disjoint, ultimately because Array.__eq__() -> Array but tuple has __eq__() -> bool?

@charliermarsh charliermarsh merged commit dba3619 into main May 30, 2026
59 checks passed
@charliermarsh charliermarsh deleted the charlie/non-never branch May 30, 2026 15:25
carljm added a commit that referenced this pull request Jun 1, 2026
* main:
  [`pydocstyle`] Improve discoverability of rules enabled for each convention (#24973)
  [ty] Deduplicate retained use-def place states (#25450)
  [ty] reduce features of low-level crates depended on by `ty_python_semantic` (#25524)
  [ty] Fix narrowing enum literal unions by member identity (#25520)
  [ty] Test tagged union narrowing for named tuples (#25519)
  [ty] Disallow file-system access in `ty_python_core` (#25518)
  [ty] Nominal Tagged Union Narrowing (#24916)
  Commit `scripts/uv.lock` (#25517)
  Fix potential index out of range in `LineIndex` computation (#25492)
  [ty] Sync vendored typeshed stubs (#25514)
  [ty] Add disjointness for protocol method members (#25315)
  [ty] Use compact sets for more immutable fields (#25476)
  [ty] Derive `Default` for `FunctionDecoratorInference` (#25482)
  [ty] Ignore rejected assignments for synthesized bindings (#25340)
  [ty] Handle cycles in function decorator inference (#25475)
  docs: fix typo `bin/active` → `bin/activate` in tutorial (#25473)
  [ty] Narrow bound method overloads by receiver (#24707)
@carljm
Copy link
Copy Markdown
Contributor

carljm commented Jun 2, 2026

Filed astral-sh/ty#3618 and astral-sh/ty#3619 with some follow-up thoughts on the unsoundness here.

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

Development

Successfully merging this pull request may close these issues.

4 participants