Skip to content

Conversation

@ericmarkmartin
Copy link
Contributor

@ericmarkmartin ericmarkmartin commented Jun 17, 2025

Summary

Implement linear-time variance inference for type variables (astral-sh/ty#488).

Inspired by Martin Huschenbett's PyCon 2025 Talk.

Test Plan

update tests
add new test case for mutually recursive classes

@ericmarkmartin ericmarkmartin force-pushed the variance-inference branch 7 times, most recently from bf98b63 to 47d348e Compare June 17, 2025 06:56
@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jun 17, 2025
@ericmarkmartin ericmarkmartin force-pushed the variance-inference branch 2 times, most recently from d3992d0 to fc1325f Compare June 18, 2025 03:36
@github-actions
Copy link
Contributor

github-actions bot commented Jun 18, 2025

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

@ericmarkmartin ericmarkmartin force-pushed the variance-inference branch 3 times, most recently from 4bb1d50 to 530d05c Compare June 23, 2025 03:45
Copy link
Member

@dcreager dcreager left a comment

Choose a reason for hiding this comment

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

Some initial comments

@AlexWaygood AlexWaygood changed the title [ty] linear variance inference [ty] linear variance inference for PEP-695 type parameters Jul 9, 2025
@ericmarkmartin ericmarkmartin force-pushed the variance-inference branch 2 times, most recently from 5102938 to 352c256 Compare July 10, 2025 22:26
@ericmarkmartin ericmarkmartin force-pushed the variance-inference branch 3 times, most recently from 6158b3a to 945aa98 Compare July 22, 2025 05:12
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

This is looking good! I think we can leave a lot of stuff as TODOs for follow-up PRs, just want to make sure we clearly record all the TODOs. I would really like to have more thorough tests here.

Regarding the failing test, I suspect what is happening is that we now see that type as bivariant in T (because T is unused), and that means that every specialization of C is equivalent to every other specialization, so the __init__ overloads no longer discriminate the instantiation argument, the overloads just all match, resulting in Unknown.

If I change the class like this (to make it covariant in T), the test passes:

class C[T]:
    @overload
    def __init__(self: C[str], x: str) -> None: ...
    @overload
    def __init__(self: C[bytes], x: bytes) -> None: ...
    @overload
    def __init__(self: C[int], x: bytes) -> None: ...
    @overload
    def __init__(self, x: int) -> None: ...
    def __init__(self, x: str | bytes | int) -> None:
        self.x = x

    def method(self) -> T:
        return self.x

@carljm
Copy link
Contributor

carljm commented Jul 24, 2025

I'm kind of shocked that there is zero ecosystem impact reported here? Is PEP 695 uptake still that low? Or maybe because we previously assumed "invariant" for PEP 695 typevars, this doesn't actually result in any new diagnostics? (Also maybe due to some issues pointed out above, e.g. the __init__ thing, in practice we are still inferring ~all PEP 695 typevars in the ecosystem as invariant?)

@ericmarkmartin ericmarkmartin force-pushed the variance-inference branch 2 times, most recently from eaf78e8 to 1b40b19 Compare August 15, 2025 03:49
@github-actions
Copy link
Contributor

github-actions bot commented Aug 15, 2025

Diagnostic diff on typing conformance tests

Changes were detected when running ty on typing conformance tests
--- old-output.txt	2025-08-20 00:39:59.104775161 +0000
+++ new-output.txt	2025-08-20 00:40:01.565795171 +0000
@@ -580,21 +580,15 @@
 generics_variance_inference.py:19:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `T3@ClassA`
 generics_variance_inference.py:24:5: error[invalid-assignment] Object of type `ClassA[int | float, int, int]` is not assignable to `ClassA[int, int, int]`
 generics_variance_inference.py:25:5: error[invalid-assignment] Object of type `ClassA[int | float, int, int]` is not assignable to `ClassA[int | float, int | float, int]`
-generics_variance_inference.py:26:5: error[invalid-assignment] Object of type `ClassA[int | float, int, int]` is not assignable to `ClassA[int | float, int, int | float]`
 generics_variance_inference.py:28:5: error[invalid-assignment] Object of type `ClassA[int, int | float, int | float]` is not assignable to `ClassA[int, int, int]`
-generics_variance_inference.py:29:5: error[invalid-assignment] Object of type `ClassA[int, int | float, int | float]` is not assignable to `ClassA[int, int, int | float]`
 generics_variance_inference.py:33:42: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `T@ShouldBeCovariant1`
 generics_variance_inference.py:36:27: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Iterator[T@ShouldBeCovariant1]`
-generics_variance_inference.py:40:1: error[invalid-assignment] Object of type `ShouldBeCovariant1[int]` is not assignable to `ShouldBeCovariant1[int | float]`
 generics_variance_inference.py:41:1: error[invalid-assignment] Object of type `ShouldBeCovariant1[int | float]` is not assignable to `ShouldBeCovariant1[int]`
-generics_variance_inference.py:48:1: error[invalid-assignment] Object of type `ShouldBeCovariant2[int]` is not assignable to `ShouldBeCovariant2[int | float]`
 generics_variance_inference.py:49:1: error[invalid-assignment] Object of type `ShouldBeCovariant2[int | float]` is not assignable to `ShouldBeCovariant2[int]`
 generics_variance_inference.py:53:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `ShouldBeCovariant2[T@ShouldBeCovariant3]`
-generics_variance_inference.py:57:1: error[invalid-assignment] Object of type `ShouldBeCovariant3[int]` is not assignable to `ShouldBeCovariant3[int | float]`
 generics_variance_inference.py:58:1: error[invalid-assignment] Object of type `ShouldBeCovariant3[int | float]` is not assignable to `ShouldBeCovariant3[int]`
 generics_variance_inference.py:66:1: error[invalid-assignment] Object of type `ShouldBeCovariant4[int]` is not assignable to `ShouldBeCovariant4[int | float]`
 generics_variance_inference.py:67:1: error[invalid-assignment] Object of type `ShouldBeCovariant4[int | float]` is not assignable to `ShouldBeCovariant4[int]`
-generics_variance_inference.py:79:1: error[invalid-assignment] Object of type `ShouldBeCovariant5[int]` is not assignable to `ShouldBeCovariant5[int | float]`
 generics_variance_inference.py:80:1: error[invalid-assignment] Object of type `ShouldBeCovariant5[int | float]` is not assignable to `ShouldBeCovariant5[int]`
 generics_variance_inference.py:96:1: error[invalid-assignment] Object of type `ShouldBeInvariant1[int]` is not assignable to `ShouldBeInvariant1[int | float]`
 generics_variance_inference.py:97:1: error[invalid-assignment] Object of type `ShouldBeInvariant1[int | float]` is not assignable to `ShouldBeInvariant1[int]`
@@ -607,12 +601,9 @@
 generics_variance_inference.py:130:1: error[invalid-assignment] Object of type `ShouldBeInvariant4[int]` is not assignable to `ShouldBeInvariant4[int | float]`
 generics_variance_inference.py:138:1: error[invalid-assignment] Object of type `ShouldBeInvariant5[int]` is not assignable to `ShouldBeInvariant5[int | float]`
 generics_variance_inference.py:149:1: error[invalid-assignment] Object of type `ShouldBeContravariant1[int]` is not assignable to `ShouldBeContravariant1[int | float]`
-generics_variance_inference.py:150:1: error[invalid-assignment] Object of type `ShouldBeContravariant1[int | float]` is not assignable to `ShouldBeContravariant1[int]`
 generics_variance_inference.py:169:1: error[invalid-assignment] Object of type `ShouldBeInvariant6[int | float]` is not assignable to `ShouldBeInvariant6[int]`
 generics_variance_inference.py:170:1: error[invalid-assignment] Object of type `ShouldBeInvariant6[int]` is not assignable to `ShouldBeInvariant6[int | float]`
 generics_variance_inference.py:181:1: error[invalid-assignment] Object of type `ShouldBeCovariant6[int | float]` is not assignable to `ShouldBeCovariant6[int]`
-generics_variance_inference.py:182:1: error[invalid-assignment] Object of type `ShouldBeCovariant6[int]` is not assignable to `ShouldBeCovariant6[int | float]`
-generics_variance_inference.py:193:1: error[invalid-assignment] Object of type `ShouldBeContravariant2[int | float]` is not assignable to `ShouldBeContravariant2[int]`
 generics_variance_inference.py:194:1: error[invalid-assignment] Object of type `ShouldBeContravariant2[int]` is not assignable to `ShouldBeContravariant2[int | float]`
 literals_interactions.py:15:5: error[index-out-of-bounds] Index 5 is out of bounds for tuple `tuple[int, str, list[bool]]` with length 3
 literals_interactions.py:16:5: error[index-out-of-bounds] Index -5 is out of bounds for tuple `tuple[int, str, list[bool]]` with length 3
@@ -859,5 +850,5 @@
 typeddicts_operations.py:60:1: error[type-assertion-failure] Argument does not have asserted type `str | None`
 typeddicts_type_consistency.py:101:1: error[invalid-assignment] Object of type `Unknown | None` is not assignable to `str`
 typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 860 diagnostics
+Found 851 diagnostics
 WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.

@ericmarkmartin ericmarkmartin marked this pull request as ready for review August 15, 2025 04:19
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Didn't quite get the whole way through reviewing this, but to me it's looking pretty much land-ready so far with some minor adjustments. I would probably just make the adjustments myself and land it now, but I'm out of time for today. Will try to get back to it as soon as I can; would love to get it landed before it accumulates more conflicts.

@carljm
Copy link
Contributor

carljm commented Aug 15, 2025

Looks like there are also a couple clippy / cargo doc issues we need to address, and then we also need to review the conformance-suite diff for correctness.

@carljm
Copy link
Contributor

carljm commented Aug 16, 2025

Finished my code review, and I reviewed the conformance suite impact. Mostly the result is good (we stop emitting wrong diagnostics where previously we assumed invariance and now we infer correct variance.)

On these lines, which are testing variance in derived classes, we now miss all the errors; it seems like we are inferring all these as bivariant now somehow? Will explore this a bit more.

ETA: Oh, I think this is the issue I mentioned inline in my code review, where when we encounter another generic class while inferring variance, we are ignoring explicit variance.

@carljm carljm force-pushed the variance-inference branch from cd80d9a to eb9908e Compare August 16, 2025 16:37
@carljm
Copy link
Contributor

carljm commented Aug 16, 2025

With the fix I just pushed, the conformance suite changes all look correct now.

@carljm
Copy link
Contributor

carljm commented Aug 16, 2025

Ok, I pushed some things here, but didn't address all the comments yet. Out of time for now. I don't have any more changes locally, so @ericmarkmartin if you have time/inclination, feel free to push changes addressing additional comments! I'll come back to this next week (but I'll comment before I make any further changes.)

@ericmarkmartin
Copy link
Contributor Author

I think I've addressed the last few comments that you didn't resolve yourself, thanks!

I tried to push out the code quickly the other night because I'm away for a little bit now and didn't want to delay the PR more, and I appreciate the thorough review. Fortunately the WiFi up here is actually quite good 😃.

I'm noticing that with the bound typevars, you write typevar and not type_var. Would you like me to update my code likewise?

@ericmarkmartin ericmarkmartin force-pushed the variance-inference branch 6 times, most recently from 9051a55 to 675b6af Compare August 18, 2025 19:55
@carljm
Copy link
Contributor

carljm commented Aug 19, 2025

I'm noticing that with the bound typevars, you write typevar and not type_var. Would you like me to update my code likewise?

I don't have strong feelings between the two, but consistency is good. Maybe a mild preference for avoiding unnecessary underscores.

@carljm
Copy link
Contributor

carljm commented Aug 19, 2025

I'm about to rebase this and make some more updates to it, hopefully resulting in a merge. (Just commenting in advance so as to avoid simultaneous duplication of work.)

ericmarkmartin and others added 2 commits August 19, 2025 15:48
optimization to re-use covariant polarity inference

refactor variance inferrence to get rid of explicit polarity

remove quotations from PyCon talk reference

fix some nits

post-rebase fixes

post-rebase fixes

clean up tests

add variance.rs

more cleanup

remove unneeded debugs

address some review comments

add tests

add property instance type variance case

fix failing classes test

add comment

collect attributes

collect all attributes

add union, intersection, and subtype tests

refactor variance composition, and TypeVar variance functions

Apply suggestions from code review

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>

clippy/doc/pre-commit fixes

fix namedtuple handling

adjust dataclass handling

respect explicit variance encountered while inferring variance

clippy

update cyclic variance inference description

fix infimum -> supremum mistake

test description

add subclassing tests

remove outdated comment in properties tests

remove test marker from is_function_literal
@carljm carljm force-pushed the variance-inference branch from 560d1f1 to 65eb68a Compare August 19, 2025 23:09
@carljm carljm merged commit 33030b3 into astral-sh:main Aug 20, 2025
38 checks passed
@carljm
Copy link
Contributor

carljm commented Aug 20, 2025

Merged! Thank you @ericmarkmartin for all your excellent work here

@ericmarkmartin ericmarkmartin deleted the variance-inference branch September 1, 2025 02:46
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.

5 participants