Skip to content

Comments

[ty] default-specialize class-literal types in assignment to generic-alias types#21883

Merged
sharkdp merged 8 commits intomainfrom
cjm/unspecassign
Dec 10, 2025
Merged

[ty] default-specialize class-literal types in assignment to generic-alias types#21883
sharkdp merged 8 commits intomainfrom
cjm/unspecassign

Conversation

@carljm
Copy link
Contributor

@carljm carljm commented Dec 10, 2025

Fixes astral-sh/ty#1832, fixes astral-sh/ty#1513

Summary

A class object C (for which we infer an unspecialized ClassLiteral type) should always be assignable to the type type[C] (which is default-specialized, if C is generic). We already implemented this for most cases, but we missed the case of a generic final type, where we simplify type[C] to the GenericAlias type for the default specialization of C. So we also need to implement this assignability of generic ClassLiteral types as-if default-specialized.

Test Plan

Added mdtests that failed before this PR.

@carljm carljm added ty Multi-file analysis & type inference ecosystem-analyzer labels Dec 10, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 10, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 10, 2025

mypy_primer results

Changes were detected when running on open source projects
mongo-python-driver (https://github.com/mongodb/mongo-python-driver)
- bson/son.py:43:31: error[invalid-assignment] Object of type `<class 'Pattern[str]'>` is not assignable to `<class 'Pattern[Any]'>`
- Found 462 diagnostics
+ Found 461 diagnostics

setuptools (https://github.com/pypa/setuptools)
- setuptools/_vendor/typing_extensions.py:3279:21: error[invalid-argument-type] Argument to bound method `register` is incorrect: Expected `<class 'memoryview[int]'>`, found `<class 'memoryview'>`
- Found 1289 diagnostics
+ Found 1288 diagnostics

trio (https://github.com/python-trio/trio)
- src/trio/_channel.py:250:16: error[invalid-argument-type] Argument to bound method `_create` is incorrect: Expected `<class 'MemorySendChannel[SendType@MemorySendChannel]'>`, found `<class 'MemorySendChannel'>`
- src/trio/_channel.py:402:16: error[invalid-argument-type] Argument to bound method `_create` is incorrect: Expected `<class 'MemoryReceiveChannel[ReceiveType@MemoryReceiveChannel]'>`, found `<class 'MemoryReceiveChannel'>`
- Found 495 diagnostics
+ Found 493 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- ddtrace/internal/settings/symbol_db.py:25:9: error[invalid-argument-type] Argument to bound method `d` is incorrect: Expected `<class 'Pattern[Unknown]'>`, found `<class 'Pattern'>`
- Found 8471 diagnostics
+ Found 8470 diagnostics

egglog-python (https://github.com/egraphs-good/egglog-python)
- python/egglog/exp/any_expr.py:643:11: error[invalid-argument-type] Argument to function `converter` is incorrect: Expected `<class 'slice[Any, Any, Any]'>`, found `<class 'slice'>`
- python/egglog/exp/array_api.py:989:5: error[invalid-argument-type] Argument to function `converter` is incorrect: Expected `<class 'slice[Any, Any, Any]'>`, found `<class 'slice'>`
- Found 1492 diagnostics
+ Found 1490 diagnostics

No memory usage changes detected ✅

@carljm
Copy link
Contributor Author

carljm commented Dec 10, 2025

Looks like this fixes a few current false positives on the ecosystem, but the main point of this is to unblock type-of-cls on classmethods, where this starts to come up a lot more.

@carljm carljm marked this pull request as ready for review December 10, 2025 02:25
@carljm carljm requested a review from ibraheemdev December 10, 2025 03:49
Copy link
Member

@dhruvmanila dhruvmanila left a comment

Choose a reason for hiding this comment

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

Can you also add similar tests for ParamSpec? I think that should just work but if not, you can add a TODO comment and I can look at it later:

from typing import final


@final
class Foo[**P]: ...


def expects_type_foo(f: type[Foo]): ...
def expects_type_foo_of_int(x: type[Foo[int]]): ...


expects_type_foo(Foo)  # ok
expects_type_foo_of_int(Foo[int])  # ok
expects_type_foo_of_int(Foo[str])  # not ok


@final
class FooDefault[**P = [int, str]]: ...


def expects_type_foo_default(f: type[FooDefault]): ...
def expects_type_foo_default_of_int(f: type[FooDefault[int]]): ...
def expects_type_foo_default_of_int_str(f: type[FooDefault[int, str]]): ...


expects_type_foo_default(FooDefault)  # ok
expects_type_foo_default(FooDefault[int, str])  # ok
expects_type_foo_default_of_int_str(FooDefault)  # ok

expects_type_foo_default(FooDefault[int])  # not ok
expects_type_foo_default_of_int(FooDefault[str])  # not ok
expects_type_foo_default_of_int_str(FooDefault[str, int])  # not ok

@sharkdp
Copy link
Contributor

sharkdp commented Dec 10, 2025

@dhruvmanila @AlexWaygood could one of you please review my changes here?

(@dhruvmanila, I noticed that some of the ParamSpec tests were correctly issuing errors before I added that new match branch, but I guess that was just an artifact? Something seems to be missing there for ParamSpec specifically)

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 10, 2025

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 0 6 0
invalid-assignment 0 1 0
unsupported-base 0 1 0
Total 0 8 0

Full report with detailed diff (timing results)

@AlexWaygood
Copy link
Member

Does this now also fix astral-sh/ty#1513? Or do we also need to make some changes to is_disjoint_from for that?

Are the new ecosystem hits on pandas expected?

@sharkdp
Copy link
Contributor

sharkdp commented Dec 10, 2025

Does this now also fix astral-sh/ty#1513? Or do we also need to make some changes to is_disjoint_from for that?

It looks like it does!

Are the new ecosystem hits on pandas expected?

Looking into it

@AlexWaygood
Copy link
Member

Does this now also fix astral-sh/ty#1513? Or do we also need to make some changes to is_disjoint_from for that?

It looks like it does!

Amazing! Is it worth adding some tests for that specifically?

@AlexWaygood
Copy link
Member

Might want to double-check our alias<->class disjointness logic anyway, in case it's now inconsistent with our subtyping logic for these types

@dhruvmanila
Copy link
Member

I noticed that some of the ParamSpec tests were correctly issuing errors before I added that new match branch, but I guess that was just an artifact? Something seems to be missing there for ParamSpec specifically)

I tried out this PR (without the new branch that you added) and it seems to be the following branch that's handling it:

            (Type::GenericAlias(alias), _) => ClassType::from(alias)
                .metaclass_instance_type(db)
                .has_relation_to_impl(
                    db,
                    target,
                    inferable,
                    relation,
                    relation_visitor,
                    disjointness_visitor,
                ),

I might have to spend some time here to understand if there's anything missing or not.

@dhruvmanila
Copy link
Member

dhruvmanila commented Dec 10, 2025

Ah, I think it's because the inferred variance of a ParamSpec is always Bivariant. We'll need to change this up to either use Invariant (which is what Pyright uses) or Covariant (which is that mypy uses) based on the following example:

from typing import final

class A: ...
class B(A): ...
class C(B): ...
class D: ...

@final
class Foo[**P]: ...

def expects_type_foo_of_b(x: type[Foo[B]]): ...

expects_type_foo_of_b(Foo[A])  # only Pyright error
expects_type_foo_of_b(Foo[B])  # ok
expects_type_foo_of_b(Foo[C])  # both Pyright and mypy error
expects_type_foo_of_b(Foo[D])  # both Pyright and mypy error

I can do it as a follow-up unless you want to add it in this PR.

Copy link
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.

LGTM, though I'd still love to double-check #21883 (comment) (do the property tests pass on this branch?)

@codspeed-hq
Copy link

codspeed-hq bot commented Dec 10, 2025

CodSpeed Performance Report

Merging #21883 will not alter performance

Comparing cjm/unspecassign (1138855) with main (7bf50e7)

Summary

✅ 22 untouched
⏩ 30 skipped1

Footnotes

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

@sharkdp
Copy link
Contributor

sharkdp commented Dec 10, 2025

LGTM, though I'd still love to double-check #21883 (comment) (do the property tests pass on this branch?)

Yes, sorry. This was not a "please review this again" push, but rather just a "I want to see if the pandas false positives are gone now" push. I didn't want to move Carl's PR to draft 😄

@sharkdp
Copy link
Contributor

sharkdp commented Dec 10, 2025

I'd still love to double-check #21883 (comment) (do the property tests pass on this branch?)

The property tests pass on this branch, which might just be because we don't have the necessary "ingredients" in the type pool. I added some tests for is_assignable_to (all of which pass), and some tests for is_disjoint_from (some of which fail, but they also fail on main). I tried for a while to implement the necessary disjointness relations, but didn't get there. I'd like to postpone this (I'll create a ticket).

@AlexWaygood
Copy link
Member

I'd like to postpone this

Fine by me!

Copy link
Contributor Author

@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.

Looks good, thank you for the additional fixes!

# Also OK, the specialized `C[int, str]` is assignable to `type[C[int, str]]`
expects_type_c_of_int_and_str(C[int, str])

# TODO: these should be errors
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we know what we are lacking here? Is there an issue to track it?

Copy link
Contributor

Choose a reason for hiding this comment

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

@dhruvmanila said he'll look into it.

Copy link
Contributor Author

@carljm carljm Dec 10, 2025

Choose a reason for hiding this comment

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

I think we should just add a use of P in the body of C, and then we will infer P as covariant (or invariant, depending what kind of use we add) and these should be fixed. That's what we do in the non-ParamSpec tests here already.

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't manage to add P in an invariant or covariant position, and adding it contravariantly does not fix the tests. After talking to Carl, we'll postpone this as well. I'll open an issue.

@carljm
Copy link
Contributor Author

carljm commented Dec 10, 2025

I think it's because the inferred variance of a ParamSpec is always Bivariant. We'll need to change this up to either use Invariant (which is what Pyright uses) or Covariant (which is that mypy uses)

I think inferring an "unused" ParamSpec type parameter (as in your example, and as in the current tests here) as bivariant is correct, according to our current variance inference logic. We do have an issue open to change this and always infer covariant for unused typevars: astral-sh/ty#1728. When we do this, it should naturally impact ParamSpec type parameters also, I think.

If we want to match pyright and always infer all ParamSpec as invariant, regardless of use, that would be a separate change specific to ParamSpec. But I'm not sure I see the rationale for that.

@sharkdp sharkdp merged commit 951766d into main Dec 10, 2025
41 checks passed
@sharkdp sharkdp deleted the cjm/unspecassign branch December 10, 2025 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

4 participants