-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] linear variance inference for PEP-695 type parameters #18713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
bf98b63 to
47d348e
Compare
d3992d0 to
fc1325f
Compare
|
4bb1d50 to
530d05c
Compare
dcreager
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial comments
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
f11b2e0 to
0c6fde2
Compare
5102938 to
352c256
Compare
6158b3a to
945aa98
Compare
carljm
left a comment
There was a problem hiding this 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
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
|
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 |
eaf78e8 to
1b40b19
Compare
Diagnostic diff on typing conformance testsChanges 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. |
crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
carljm
left a comment
There was a problem hiding this 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.
crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md
Outdated
Show resolved
Hide resolved
|
Looks like there are also a couple clippy / |
|
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. |
cd80d9a to
eb9908e
Compare
|
With the fix I just pushed, the conformance suite changes all look correct now. |
|
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.) |
|
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 |
9051a55 to
675b6af
Compare
I don't have strong feelings between the two, but consistency is good. Maybe a mild preference for avoiding unnecessary underscores. |
|
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.) |
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
560d1f1 to
65eb68a
Compare
|
Merged! Thank you @ericmarkmartin for all your excellent work here |
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