Skip to content

[ty] Infer variance through recursive protocols - #28077

Open
charliermarsh wants to merge 6 commits into
mainfrom
charlie/recursive-protocol-variance
Open

[ty] Infer variance through recursive protocols#28077
charliermarsh wants to merge 6 commits into
mainfrom
charlie/recursive-protocol-variance

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Aug 26, 2026

Copy link
Copy Markdown
Member

Summary

We now infer and validate variance through direct, mutual, and expanding recursive protocol references. A recursive member no longer suppresses the variance mismatch in this protocol:

from typing import Protocol, TypeVar

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

class Sink(Protocol[T_co]):  # error: should be contravariant
    def write(self, value: T_co) -> None: ...
    def next(self) -> "Sink[T_co]": ...

We separate constructing variance equations from evaluating them. Recursive references name another parameter’s equation instead of expanding its definition, so even references like P[list[T]] produce a finite problem.

To validate a declaration, we solve mutually dependent parameters together, starting at bivariance and updating until the results stop changing. This prevents a protocol’s declared variance from serving as evidence for its own correctness. Declarations outside that recursive group remain authoritative. Dependencies also respect argument erasure: an alias that ignores its type argument does not create a recursive dependency through that argument. Ordinary type inference continues to honor declared variance, including for unused parameters.

The symbolic terms and fixed-point iteration follow the general approach described in the Rust compiler’s variance-inference documentation, adapted here to validating Python protocol declarations.

This also enables structural variance inference for recursive PEP 695 protocols, including invariance from writable attributes. Recursive type aliases and descriptor setters whose accepted values cannot be represented by a single type remain deferred.

@charliermarsh
charliermarsh requested a review from a team as a code owner August 26, 2026 16:28
@charliermarsh charliermarsh added the ty Multi-file analysis & type inference label Aug 26, 2026
@astral-sh-bot
astral-sh-bot Bot requested a review from dhruvmanila August 26, 2026 16:29
@charliermarsh
charliermarsh marked this pull request as draft August 26, 2026 16:29
Base automatically changed from charlie/protocol-variance-recursion to main August 26, 2026 16:54
@carljm

carljm commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

This would help fix a user-reported issue in astral-sh/ty#4407

@charliermarsh

Copy link
Copy Markdown
Member Author

I'll revive it, thanks!

@charliermarsh
charliermarsh force-pushed the charlie/recursive-protocol-variance branch from 2cc7164 to 93b39a5 Compare August 27, 2026 23:21
@astral-sh-bot

astral-sh-bot Bot commented Aug 27, 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 97.79%. The percentage of expected errors that received a diagnostic held steady at 94.33%. The number of fully passing files held steady at 112/136.

@astral-sh-bot

astral-sh-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 478.16MB 478.52MB +0.08% (373.91kB)
trio 96.99MB 97.20MB +0.22% (221.47kB)
sphinx 172.94MB 173.13MB +0.11% (190.14kB)
flake8 40.23MB 40.25MB +0.04% (17.82kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
StaticClassLiteral<'db>::variance_of_owner_ 248.80kB 0.00B -100.00% (248.80kB) ⬇️
VarianceVariable 0.00B 182.34kB +182.34kB (new)
VarianceVariable<'db>::effective_variance_ 0.00B 161.32kB +161.32kB (new)
VarianceVariable<'db>::equation_ 0.00B 98.11kB +98.11kB (new)
StaticClassLiteral<'db>::variance_equation_::interned_arguments 0.00B 91.61kB +91.61kB (new)
StaticClassLiteral<'db>::variance_of_owner_::interned_arguments 90.75kB 0.00B -100.00% (90.75kB) ⬇️
StaticClassLiteral<'db>::variance_equation_ 0.00B 76.72kB +76.72kB (new)
GenericAlias<'db>::variance_equation_::interned_arguments 0.00B 66.60kB +66.60kB (new)
GenericAlias<'db>::variance_of_owner_ 66.48kB 0.00B -100.00% (66.48kB) ⬇️
GenericAlias<'db>::variance_of_owner_::interned_arguments 65.31kB 0.00B -100.00% (65.31kB) ⬇️
VarianceSum 0.00B 54.09kB +54.09kB (new)
VarianceProduct 0.00B 38.59kB +38.59kB (new)
GenericAlias<'db>::variance_equation_ 0.00B 38.20kB +38.20kB (new)
CallableType 8.57MB 8.58MB +0.10% (8.70kB)
infer_definition_types 37.04MB 37.05MB +0.01% (4.89kB)
... 45 more

trio

Name Old New Diff Outcome
VarianceVariable 0.00B 84.38kB +84.38kB (new)
VarianceVariable<'db>::equation_ 0.00B 45.67kB +45.67kB (new)
StaticClassLiteral<'db>::variance_equation_::interned_arguments 0.00B 45.20kB +45.20kB (new)
StaticClassLiteral<'db>::variance_of_owner_::interned_arguments 44.43kB 0.00B -100.00% (44.43kB) ⬇️
VarianceVariable<'db>::effective_variance_ 0.00B 39.75kB +39.75kB (new)
GenericAlias<'db>::variance_equation_::interned_arguments 0.00B 31.71kB +31.71kB (new)
GenericAlias<'db>::variance_of_owner_::interned_arguments 31.45kB 0.00B -100.00% (31.45kB) ⬇️
StaticClassLiteral<'db>::variance_equation_ 0.00B 28.69kB +28.69kB (new)
StaticClassLiteral<'db>::variance_of_owner_ 23.98kB 0.00B -100.00% (23.98kB) ⬇️
GenericAlias<'db>::variance_equation_ 0.00B 19.33kB +19.33kB (new)
GenericAlias<'db>::variance_of_owner_ 15.94kB 0.00B -100.00% (15.94kB) ⬇️
CallableType 1.44MB 1.44MB +0.62% (9.08kB)
VarianceSum 0.00B 7.64kB +7.64kB (new)
infer_expression_types_impl 6.42MB 6.42MB +0.11% (7.49kB)
VarianceProduct 0.00B 5.94kB +5.94kB (new)
... 37 more

sphinx

Name Old New Diff Outcome
VarianceVariable 0.00B 75.19kB +75.19kB (new)
VarianceVariable<'db>::equation_ 0.00B 38.80kB +38.80kB (new)
StaticClassLiteral<'db>::variance_equation_::interned_arguments 0.00B 36.27kB +36.27kB (new)
StaticClassLiteral<'db>::variance_of_owner_::interned_arguments 35.92kB 0.00B -100.00% (35.92kB) ⬇️
VarianceVariable<'db>::effective_variance_ 0.00B 35.06kB +35.06kB (new)
GenericAlias<'db>::variance_equation_::interned_arguments 0.00B 26.90kB +26.90kB (new)
GenericAlias<'db>::variance_of_owner_::interned_arguments 26.47kB 0.00B -100.00% (26.47kB) ⬇️
StaticClassLiteral<'db>::variance_equation_ 0.00B 24.60kB +24.60kB (new)
StaticClassLiteral<'db>::variance_of_owner_ 19.67kB 0.00B -100.00% (19.67kB) ⬇️
GenericAlias<'db>::variance_equation_ 0.00B 15.33kB +15.33kB (new)
GenericAlias<'db>::variance_of_owner_ 13.90kB 0.00B -100.00% (13.90kB) ⬇️
VarianceSum 0.00B 12.62kB +12.62kB (new)
VarianceProduct 0.00B 11.09kB +11.09kB (new)
place_by_id 1.16MB 1.16MB +0.25% (3.00kB)
place_by_id::interned_arguments 953.58kB 955.83kB +0.24% (2.25kB)
... 25 more

flake8

Name Old New Diff Outcome
VarianceVariable 0.00B 7.59kB +7.59kB (new)
StaticClassLiteral<'db>::variance_of_owner_::interned_arguments 4.90kB 0.00B -100.00% (4.90kB) ⬇️
StaticClassLiteral<'db>::variance_equation_::interned_arguments 0.00B 4.90kB +4.90kB (new)
VarianceVariable<'db>::equation_ 0.00B 4.04kB +4.04kB (new)
VarianceVariable<'db>::effective_variance_ 0.00B 3.53kB +3.53kB (new)
StaticClassLiteral<'db>::variance_equation_ 0.00B 2.96kB +2.96kB (new)
StaticClassLiteral<'db>::variance_of_owner_ 2.52kB 0.00B -100.00% (2.52kB) ⬇️
GenericAlias<'db>::variance_equation_::interned_arguments 0.00B 2.06kB +2.06kB (new)
GenericAlias<'db>::variance_of_owner_::interned_arguments 2.06kB 0.00B -100.00% (2.06kB) ⬇️
GenericAlias<'db>::variance_equation_ 0.00B 1.17kB +1.17kB (new)
VarianceProduct 0.00B 1.09kB +1.09kB (new)
GenericAlias<'db>::variance_of_owner_ 1.09kB 0.00B -100.00% (1.09kB) ⬇️
VarianceSum 0.00B 1008.00B +1008.00B (new)
infer_definition_types 1.12MB 1.12MB +0.00% (32.00B)
infer_scope_types_impl 533.91kB 533.92kB +0.00% (8.00B)
... 1 more

@astral-sh-bot

astral-sh-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-protocol 3 0 0
Total 3 0 0

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Raw diff:

antidote (https://github.com/Finistere/antidote)
+ src/antidote/core/__init__.py:1300:7 error[invalid-protocol] Type variable `T` in protocol `InjectedMethod` should be covariant, but is invariant

comtypes (https://github.com/enthought/comtypes)
+ comtypes/hints.pyi:236:7 error[invalid-protocol] Type variable `_R_Get` in protocol `_Descriptor` should be covariant, but is invariant
+ comtypes/hints.pyi:236:7 error[invalid-protocol] Type variable `_T_Inst` in protocol `_Descriptor` should be contravariant, but is invariant

Full report with detailed diff (timing results)

@charliermarsh
charliermarsh force-pushed the charlie/recursive-protocol-variance branch from cfea82c to 0bad8b7 Compare August 28, 2026 01:12
@charliermarsh
charliermarsh requested review from carljm and removed request for dhruvmanila August 28, 2026 01:13
@charliermarsh
charliermarsh marked this pull request as ready for review August 28, 2026 01:28
@codspeed-hq

codspeed-hq Bot commented Aug 28, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 5.07%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 124 untouched benchmarks
⏩ 84 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation ty_micro[inherited_recursive_protocol_constructor] 10.4 s 9.8 s +5.1%
Simulation ty_micro[inherited_recursive_protocol_diagnostic] 10.4 s 9.9 s +5.04%

Tip

Curious why performance improved? Use the CodSpeed MCP and ask your agent.


Comparing charlie/recursive-protocol-variance (de54cd1) with main (1776127)

Open in CodSpeed

Footnotes

  1. 84 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.

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.

divergence in protocol variance inference between ty and pyright

2 participants