Skip to content

Conversation

@oconnor663
Copy link
Contributor

Closes astral-sh/ty#1101.

As I push this PR, there is one failing test line on my box, which I need help interpreting. Here's an extracted version of it:

from ty_extensions import static_assert, is_assignable_to, TypeOf
from typing import Generic, TypeVar

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

class Foo(Generic[T_co]): ...
class Bar(Foo[T_co], Generic[T_co]): ...

static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))  # error: false

That static_assert fails. The "never" result seems to originate on this line, which I read as "no non-generic class literal is ever assignable/etc to a specialization" (not sure how to word that correctly). That...seems not true...?...so I'm wondering I've somehow skipped the step where the non-generic class literal, which does in fact have generic parameters, gets them implicitly specialized to object? I'm betting this will be super obvious to someone :)

@oconnor663
Copy link
Contributor Author

Also thanks @carljm for talking me through the implementation of this :)

@oconnor663 oconnor663 added the ty Multi-file analysis & type inference label Nov 21, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 21, 2025

Diagnostic diff on typing conformance tests

Changes were detected when running ty on typing conformance tests
--- old-output.txt	2025-11-21 21:46:55.218257234 +0000
+++ new-output.txt	2025-11-21 21:46:58.715273992 +0000
@@ -220,6 +220,7 @@
 constructors_call_new.py:117:1: error[type-assertion-failure] Type `Class8[list[str]]` does not match asserted type `Class8[str]`
 constructors_call_new.py:125:42: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Self@__new__`
 constructors_call_new.py:140:47: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Class11[int]`
+constructors_call_new.py:145:1: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `type[Class11[int]]`, found `<class 'Class11[str]'>`
 constructors_call_type.py:40:5: error[missing-argument] No arguments provided for required parameters `x`, `y` of function `__new__`
 constructors_call_type.py:50:5: error[missing-argument] No arguments provided for required parameters `x`, `y` of bound method `__init__`
 constructors_call_type.py:59:9: error[too-many-positional-arguments] Too many positional arguments to bound method `__init__`: expected 1, got 2
@@ -415,25 +416,26 @@
 generics_basic.py:171:1: error[invalid-generic-class] `Generic` base class must include all type variables used in other base classes
 generics_basic.py:172:1: error[invalid-generic-class] `Generic` base class must include all type variables used in other base classes
 generics_basic.py:199:5: error[type-assertion-failure] Type `Iterator[Any]` does not match asserted type `Unknown`
-generics_defaults.py:30:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'NoNonDefaults'>`
-generics_defaults.py:31:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'NoNonDefaults[str, int]'>`
-generics_defaults.py:32:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'NoNonDefaults[str, int]'>`
-generics_defaults.py:38:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'OneDefault[int | float, bool]'>`
-generics_defaults.py:45:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'AllTheDefaults'>`
-generics_defaults.py:46:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
+generics_defaults.py:30:1: error[type-assertion-failure] Type `type[NoNonDefaults[str, int]]` does not match asserted type `<class 'NoNonDefaults'>`
+generics_defaults.py:31:1: error[type-assertion-failure] Type `type[NoNonDefaults[str, int]]` does not match asserted type `<class 'NoNonDefaults[str, int]'>`
+generics_defaults.py:32:1: error[type-assertion-failure] Type `type[NoNonDefaults[str, int]]` does not match asserted type `<class 'NoNonDefaults[str, int]'>`
+generics_defaults.py:38:1: error[type-assertion-failure] Type `type[OneDefault[int | float, bool]]` does not match asserted type `<class 'OneDefault[int | float, bool]'>`
+generics_defaults.py:45:1: error[type-assertion-failure] Type `type[AllTheDefaults[Any, Any, str, int, bool]]` does not match asserted type `<class 'AllTheDefaults'>`
+generics_defaults.py:46:1: error[type-assertion-failure] Type `type[AllTheDefaults[int, int | float | complex, str, int, bool]]` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
 generics_defaults.py:50:1: error[missing-argument] No argument provided for required parameter `T2` of class `AllTheDefaults`
-generics_defaults.py:52:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
-generics_defaults.py:55:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
-generics_defaults.py:59:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
-generics_defaults.py:63:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
-generics_defaults.py:79:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'Class_ParamSpec'>`
+generics_defaults.py:52:1: error[type-assertion-failure] Type `type[AllTheDefaults[int, int | float | complex, str, int, bool]]` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
+generics_defaults.py:55:1: error[type-assertion-failure] Type `type[AllTheDefaults[int, int | float | complex, str, int, bool]]` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
+generics_defaults.py:59:1: error[type-assertion-failure] Type `type[AllTheDefaults[int, int | float | complex, str, int, bool]]` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
+generics_defaults.py:63:1: error[type-assertion-failure] Type `type[AllTheDefaults[int, int | float | complex, str, int, bool]]` does not match asserted type `<class 'AllTheDefaults[int, int | float | complex, str, int, bool]'>`
+generics_defaults.py:79:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `<class 'Class_ParamSpec'>`
+generics_defaults.py:79:35: error[too-many-positional-arguments] Too many positional arguments to class `Class_ParamSpec`: expected 1, got 2
 generics_defaults.py:80:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `Class_ParamSpec[Unknown]`
 generics_defaults.py:80:32: error[too-many-positional-arguments] Too many positional arguments to class `Class_ParamSpec`: expected 1, got 2
 generics_defaults.py:81:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `Class_ParamSpec[Unknown]`
 generics_defaults.py:81:29: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[bool, bool]`?
 generics_defaults.py:81:46: error[too-many-positional-arguments] Too many positional arguments to class `Class_ParamSpec`: expected 1, got 2
 generics_defaults.py:91:26: error[invalid-argument-type] `@Todo(starred expression)` is not a valid argument to `Generic`
-generics_defaults.py:94:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'Class_TypeVarTuple'>`
+generics_defaults.py:94:1: error[type-assertion-failure] Type `@Todo(specialized non-generic class)` does not match asserted type `<class 'Class_TypeVarTuple'>`
 generics_defaults.py:95:1: error[type-assertion-failure] Type `@Todo(specialized non-generic class)` does not match asserted type `Class_TypeVarTuple`
 generics_defaults.py:127:32: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `T4@func1`
 generics_defaults.py:131:1: error[type-assertion-failure] Type `Any` does not match asserted type `int`
@@ -442,15 +444,15 @@
 generics_defaults.py:155:49: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[int | float, bool]`?
 generics_defaults.py:156:58: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `list[bytes]`?
 generics_defaults.py:170:1: error[type-assertion-failure] Type `(Foo7[int], /) -> Foo7[int]` does not match asserted type `def meth(self, /) -> Self@meth`
-generics_defaults_referential.py:23:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'slice'>`
+generics_defaults_referential.py:23:1: error[type-assertion-failure] Type `type[slice[int, int, int | None]]` does not match asserted type `<class 'slice'>`
 generics_defaults_referential.py:36:13: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `int`, found `Literal[""]`
 generics_defaults_referential.py:37:10: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `int`, found `Literal[""]`
-generics_defaults_referential.py:94:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'Bar'>`
-generics_defaults_referential.py:95:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'Bar[int, list[int]]'>`
+generics_defaults_referential.py:94:1: error[type-assertion-failure] Type `type[Bar[Any, list[Any]]]` does not match asserted type `<class 'Bar'>`
+generics_defaults_referential.py:95:1: error[type-assertion-failure] Type `type[Bar[int, list[int]]]` does not match asserted type `<class 'Bar[int, list[int]]'>`
 generics_defaults_specialization.py:26:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, str]` does not match asserted type `SomethingWithNoDefaults[int, typing.TypeVar]`
 generics_defaults_specialization.py:27:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, bool]` does not match asserted type `@Todo(specialized generic alias in type expression)`
 generics_defaults_specialization.py:30:1: error[non-subscriptable] Cannot subscript object of type `<class 'SomethingWithNoDefaults[int, typing.TypeVar]'>` with no `__class_getitem__` method
-generics_defaults_specialization.py:45:1: error[type-assertion-failure] Type `@Todo(unsupported nested subscript in type[X])` does not match asserted type `<class 'Bar'>`
+generics_defaults_specialization.py:45:1: error[type-assertion-failure] Type `type[Bar[str]]` does not match asserted type `<class 'Bar'>`
 generics_paramspec_basic.py:10:1: error[invalid-paramspec] The name of a `ParamSpec` (`NotIt`) must match the name of the variable it is assigned to (`WrongName`)
 generics_paramspec_basic.py:23:20: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `P@func1`
 generics_paramspec_basic.py:27:38: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
@@ -1001,5 +1003,5 @@
 typeddicts_usage.py:28:17: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
 typeddicts_usage.py:28:18: error[invalid-key] Unknown key "title" for TypedDict `Movie`: Unknown key "title"
 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 1003 diagnostics
+Found 1005 diagnostics
 WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.

@carljm
Copy link
Contributor

carljm commented Nov 21, 2025

The implicit specialization should use the default_specialization method, and should not specialize to object but to Unknown (or the typevar default, if any, but there is none in this example).

To be honest I'm not sure if we want TypeOf[Bar] to implicitly default-specialize Bar, though. TypeOf is a test introspection helper designed to say "give me the type for this name", and I think it might break some other uses of it if it always implicitly default-specialized instead of giving the "raw" type of that name.

So I suspect the better option here might be to change that test assertion, with a comment saying something like "TypeOf does not implicitly default-specialize. The unspecialized class literal object Bar does not inhabit the type type[Foo[int]]."

And then add another test asserting explicitly that type[Bar[Unknown]] is assignable to type[Foo[int]], if we don't already have that version.

I think the line you pointed to in ClassType::has_relation_to_impl is correct. What it is saying is that I am only a subclass of other if I inherit from a base class with matching specialization with other -- if other is generic and my base is not (or vice versa), that cannot make me a subclass of other. Note that we are in a when_any(...) iteration of self.iter_mro(db) there.

static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Unknown]], type[Foo[int]]))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this new Unknown case redundant with the Any case right above it, or are is their behavior different enough that it makes sense to test both?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's redundant, we very rarely handle Unknown differently from Any. But there's also no harm in including it, don't really care either way.

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.

Ship it!

static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Unknown]], type[Foo[int]]))
Copy link
Contributor

Choose a reason for hiding this comment

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

It's redundant, we very rarely handle Unknown differently from Any. But there's also no harm in including it, don't really care either way.

@AlexWaygood
Copy link
Member

Ship it!

Looks like we're panicking on a mypy_primer project in CI?

@carljm
Copy link
Contributor

carljm commented Nov 21, 2025

Nice catch :) Ok then, don't ship it quite yet...

@oconnor663
Copy link
Contributor Author

oconnor663 commented Nov 21, 2025

What's one little panic between friends?

@oconnor663
Copy link
Contributor Author

Progress, this seems to be a minimal repro:

class Foo:
    pass

_: type[Foo[int]]
error[panic]: Panicked at crates/ty_python_semantic/src/types/infer/builder.rs:6946:14 when checking `/tmp/test.py`: `assertion `left == right` failed
  left: Some(ClassLiteral(ClassLiteral { name: Name("Foo"), body_scope: ScopeId { [salsa id]: Id(1001), file: File(System("/tmp/test.py")), file_scope_id: FileScopeId(1) }, known: None, deprecated: None, type_check_only: false, dataclass_params: None, dataclass_transformer_params: None }))
 right: None`
info: This indicates a bug in ty.
info: If you could open an issue at https://github.com/astral-sh/ty/issues/new?title=%5Bpanic%5D, we'd be very appreciative!
info: Platform: linux x86_64
info: Version: ruff/0.14.5+81 (502618634 2025-11-21)
info: Args: ["/home/jacko/astral/ruff/target-mold/debug/ty", "check", "test.py"]
info: run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information
info: query stacktrace:
   0: infer_definition_types(Id(1401))
             at crates/ty_python_semantic/src/types/infer.rs:94
   1: infer_scope_types(Id(1000))
             at crates/ty_python_semantic/src/types/infer.rs:70
   2: check_file_impl(Id(c00))
             at crates/ty_project/src/lib.rs:535

I think this is the panic that we get when something is inferred more than once. Did it used to have a better error message than this?

}
None => {
// TODO: emit a diagnostic if you try to specialize a non-generic class.
self.infer_type_expression(slice);
Copy link
Contributor

@carljm carljm Nov 21, 2025

Choose a reason for hiding this comment

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

This looks like the likely cause of the panic, slice includes value which we already inferred above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep already there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting that the line in the wild that triggers this is https://github.com/python-jsonschema/referencing/blob/d97e65e86c95d51f01e18d89461da6d4a0c4e72a/referencing/_core.py#L200

        default_specification: (
            type[Specification[D]] | Specification[D]
        ) = Specification,

But in this context, Specification is generic...

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, that's strange and deserving of further investigation?

But doesn't need to block landing this IMO.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 21, 2025

mypy_primer results

Changes were detected when running on open source projects
pip (https://github.com/pypa/pip)
- src/pip/_vendor/truststore/_macos.py:294:56: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- src/pip/_vendor/truststore/_macos.py:314:71: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- src/pip/_vendor/truststore/_macos.py:337:76: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 583 diagnostics
+ Found 580 diagnostics

werkzeug (https://github.com/pallets/werkzeug)
+ src/werkzeug/sansio/request.py:75:60: error[invalid-assignment] Object of type `<class 'ImmutableMultiDict'>` is not assignable to `type[MultiDict[str, Any]]`
+ src/werkzeug/sansio/request.py:85:55: error[invalid-assignment] Object of type `<class 'ImmutableMultiDict'>` is not assignable to `type[MultiDict[str, Any]]`
+ tests/test_formparser.py:437:76: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `type[MultiDict[str, Any]] | None`, found `<class 'dict'>`
- Found 411 diagnostics
+ Found 414 diagnostics

pytest (https://github.com/pytest-dev/pytest)
+ src/_pytest/capture.py:940:17: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Argument type `AnyStr@CaptureFixture` does not satisfy constraints (`str`, `bytes`) of type variable `AnyStr`
+ src/_pytest/capture.py:941:17: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Argument type `AnyStr@CaptureFixture` does not satisfy constraints (`str`, `bytes`) of type variable `AnyStr`
- Found 449 diagnostics
+ Found 451 diagnostics

mkosi (https://github.com/systemd/mkosi)
- mkosi/config.py:5862:43: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 97 diagnostics
+ Found 96 diagnostics

psycopg (https://github.com/psycopg/psycopg)
+ psycopg/psycopg/connection.py:78:9: error[invalid-assignment] Object of type `<class 'Cursor'>` is not assignable to attribute `cursor_factory` of type `type[Cursor[Row@Connection]]`
+ psycopg/psycopg/connection.py:79:9: error[invalid-assignment] Object of type `<class 'ServerCursor'>` is not assignable to attribute `server_cursor_factory` of type `type[ServerCursor[Row@Connection]]`
+ psycopg/psycopg/connection_async.py:82:9: error[invalid-assignment] Object of type `<class 'AsyncCursor'>` is not assignable to attribute `cursor_factory` of type `type[AsyncCursor[Row@AsyncConnection]]`
+ psycopg/psycopg/connection_async.py:83:9: error[invalid-assignment] Object of type `<class 'AsyncServerCursor'>` is not assignable to attribute `server_cursor_factory` of type `type[AsyncServerCursor[Row@AsyncConnection]]`
+ psycopg/psycopg/types/composite.py:539:12: error[invalid-return-type] Return type does not match returned value: expected `type[_CompositeLoader[T@_make_loader]]`, found `type`
+ psycopg/psycopg/types/composite.py:556:12: error[invalid-return-type] Return type does not match returned value: expected `type[_CompositeBinaryLoader[T@_make_binary_loader]]`, found `type`
+ psycopg/psycopg/types/composite.py:575:12: error[invalid-return-type] Return type does not match returned value: expected `type[_SequenceDumper[T@_make_dumper]]`, found `type`
+ psycopg/psycopg/types/composite.py:594:12: error[invalid-return-type] Return type does not match returned value: expected `type[_SequenceBinaryDumper[T@_make_binary_dumper]]`, found `type`
+ psycopg/psycopg/types/enum.py:183:12: error[invalid-return-type] Return type does not match returned value: expected `type[_BaseEnumLoader[E@_make_loader]]`, found `type`
+ psycopg/psycopg/types/enum.py:191:12: error[invalid-return-type] Return type does not match returned value: expected `type[_BaseEnumLoader[E@_make_binary_loader]]`, found `type`
+ psycopg/psycopg/types/enum.py:199:12: error[invalid-return-type] Return type does not match returned value: expected `type[_BaseEnumDumper[E@_make_dumper]]`, found `type`
+ psycopg/psycopg/types/enum.py:207:12: error[invalid-return-type] Return type does not match returned value: expected `type[_BaseEnumDumper[E@_make_binary_dumper]]`, found `type`
+ psycopg/psycopg/types/multirange.py:407:12: error[invalid-return-type] Return type does not match returned value: expected `type[MultirangeLoader[Any]]`, found `type`
+ psycopg/psycopg/types/multirange.py:412:12: error[invalid-return-type] Return type does not match returned value: expected `type[MultirangeBinaryLoader[Any]]`, found `type`
+ psycopg/psycopg/types/range.py:586:12: error[invalid-return-type] Return type does not match returned value: expected `type[RangeLoader[Any]]`, found `type`
+ psycopg/psycopg/types/range.py:591:12: error[invalid-return-type] Return type does not match returned value: expected `type[RangeBinaryLoader[Any]]`, found `type`
- Found 649 diagnostics
+ Found 665 diagnostics

comtypes (https://github.com/enthought/comtypes)
- comtypes/_post_coinit/activeobj.py:45:42: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/_post_coinit/activeobj.py:46:15: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/_post_coinit/misc.py:106:18: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/_post_coinit/misc.py:142:15: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/client/__init__.py:29:80: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/errorinfo.py:90:17: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/errorinfo.py:97:25: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ comtypes/safearray.py:393:13: error[invalid-super-argument] `Self@__setitem__` is not an instance or subclass of `type[_Pointer[Unknown]]` in `super(type[_Pointer[Unknown]], Self@__setitem__)` call
+ comtypes/test/test_basic.py:31:30: error[unresolved-attribute] Object of type `_Pointer[Unknown]` has no attribute `AddRef`
+ comtypes/test/test_basic.py:33:30: error[unresolved-attribute] Object of type `_Pointer[Unknown]` has no attribute `Release`
+ comtypes/test/test_basic.py:40:26: error[unresolved-attribute] Object of type `_Pointer[Unknown]` has no attribute `AddRef`
+ comtypes/test/test_basic.py:41:26: error[unresolved-attribute] Object of type `_Pointer[Unknown]` has no attribute `Release`
+ comtypes/test/test_basic.py:43:17: error[unresolved-attribute] Object of type `_Pointer[Unknown]` has no attribute `QueryInterface`
+ comtypes/test/test_basic.py:45:26: error[unresolved-attribute] Object of type `_Pointer[Unknown]` has no attribute `AddRef`
+ comtypes/test/test_basic.py:46:26: error[unresolved-attribute] Object of type `_Pointer[Unknown]` has no attribute `Release`
+ comtypes/test/test_basic.py:89:9: error[unresolved-attribute] Object of type `type[_Pointer[Unknown]]` has no attribute `QueryInterface`
- comtypes/test/test_comobject.py:78:44: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/test/test_comobject.py:79:45: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/test/test_comobject.py:80:54: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ comtypes/test/test_outparam.py:62:12: error[unresolved-attribute] Object of type `_Pointer[Unknown]` has no attribute `DidAlloc`
- comtypes/typeinfo.py:291:40: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/typeinfo.py:439:47: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/typeinfo.py:468:24: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/typeinfo.py:659:16: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/typeinfo.py:674:16: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/typeinfo.py:683:18: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/typeinfo.py:690:17: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/typeinfo.py:697:18: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- comtypes/typeinfo.py:722:19: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 354 diagnostics
+ Found 345 diagnostics

mypy (https://github.com/python/mypy)
+ mypy/typeshed/stdlib/collections/__init__.pyi:43:11: error[too-many-positional-arguments] Too many positional arguments to class `tuple`: expected 1, got 2
- Found 1587 diagnostics
+ Found 1588 diagnostics

mongo-python-driver (https://github.com/mongodb/mongo-python-driver)
- bson/json_util.py:527:75: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ bson/codec_options.py:400:45: error[invalid-argument-type] Argument to function `issubclass` is incorrect: Expected `type`, found `object`
- bson/regex.py:83:16: error[invalid-return-type] Return type does not match returned value: expected `Regex[_T@Regex]`, found `Regex[str]`
+ bson/son.py:43:31: error[invalid-assignment] Object of type `<class 'Pattern[str]'>` is not assignable to `<class 'Pattern[Any]'>`
- pymongo/asynchronous/auth.py:131:43: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `bytes | bytearray`, found `@Todo | None | bytes`
+ pymongo/asynchronous/auth.py:131:43: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `bytes | bytearray`, found `Unknown | None | bytes`
- pymongo/asynchronous/auth.py:365:69: error[invalid-assignment] Object of type `dict[Unknown | str, Unknown | ((credentials: @Todo, conn: AsyncConnection) -> CoroutineType[Any, Any, None]) | ((credentials: @Todo, conn: AsyncConnection, reauthenticate: bool) -> CoroutineType[Any, Any, Mapping[str, Any] | None]) | partial[Unknown]]` is not assignable to `Mapping[str, (...) -> Coroutine[Any, Any, None]]`
+ pymongo/asynchronous/auth.py:365:69: error[invalid-assignment] Object of type `dict[Unknown | str, Unknown | ((credentials: Unknown, conn: AsyncConnection) -> CoroutineType[Any, Any, None]) | ((credentials: Unknown, conn: AsyncConnection, reauthenticate: bool) -> CoroutineType[Any, Any, Mapping[str, Any] | None]) | partial[Unknown]]` is not assignable to `Mapping[str, (...) -> Coroutine[Any, Any, None]]`
- pymongo/synchronous/auth.py:128:43: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `bytes | bytearray`, found `@Todo | None | bytes`
+ pymongo/synchronous/auth.py:128:43: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `bytes | bytearray`, found `Unknown | None | bytes`
- pymongo/synchronous/auth.py:360:48: error[invalid-assignment] Object of type `dict[Unknown | str, Unknown | ((credentials: @Todo, conn: Connection) -> None) | ((credentials: @Todo, conn: Connection, reauthenticate: bool) -> Mapping[str, Any] | None) | partial[Unknown]]` is not assignable to `Mapping[str, (...) -> None]`
+ pymongo/synchronous/auth.py:360:48: error[invalid-assignment] Object of type `dict[Unknown | str, Unknown | ((credentials: Unknown, conn: Connection) -> None) | ((credentials: Unknown, conn: Connection, reauthenticate: bool) -> Mapping[str, Any] | None) | partial[Unknown]]` is not assignable to `Mapping[str, (...) -> None]`

hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
- src/hydra_zen/structured_configs/_implementations.py:668:8: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ tests/annotations/behaviors.py:50:29: error[unresolved-attribute] Object of type `Path` has no attribute `x`
+ tests/annotations/behaviors.py:54:26: error[invalid-assignment] Object of type `Path` is not assignable to `int`
- tests/annotations/declarations.py:122:9: info[revealed-type] Revealed type: `@Todo`
+ tests/annotations/declarations.py:122:9: info[revealed-type] Revealed type: `type[@Todo]`
- tests/annotations/declarations.py:233:9: info[revealed-type] Revealed type: `@Todo`
+ tests/annotations/declarations.py:233:9: info[revealed-type] Revealed type: `type[@Todo]`
+ tests/annotations/declarations.py:501:16: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `type[@Todo]`
+ tests/annotations/declarations.py:503:16: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `type[@Todo]`
- tests/annotations/mypy_checks.py:18:5: error[type-assertion-failure] Type `A` does not match asserted type `Unknown`
+ tests/annotations/mypy_checks.py:18:5: error[type-assertion-failure] Type `A` does not match asserted type `Path`
- tests/annotations/mypy_checks.py:19:5: error[type-assertion-failure] Type `A` does not match asserted type `Unknown`
+ tests/annotations/mypy_checks.py:19:5: error[type-assertion-failure] Type `A` does not match asserted type `Path`
- tests/annotations/mypy_checks.py:22:5: error[type-assertion-failure] Type `str` does not match asserted type `Unknown`
+ tests/annotations/mypy_checks.py:22:5: error[type-assertion-failure] Type `str` does not match asserted type `Path`
- tests/annotations/mypy_checks.py:23:5: error[type-assertion-failure] Type `str` does not match asserted type `Unknown`
+ tests/annotations/mypy_checks.py:23:5: error[type-assertion-failure] Type `str` does not match asserted type `Path`
- tests/annotations/mypy_checks.py:42:5: error[type-assertion-failure] Type `A` does not match asserted type `Unknown`
+ tests/annotations/mypy_checks.py:42:5: error[type-assertion-failure] Type `A` does not match asserted type `Path`
- tests/annotations/mypy_checks.py:43:5: error[type-assertion-failure] Type `A` does not match asserted type `Unknown`
+ tests/annotations/mypy_checks.py:43:5: error[type-assertion-failure] Type `A` does not match asserted type `Path`
- tests/annotations/mypy_checks.py:46:5: error[type-assertion-failure] Type `str` does not match asserted type `Unknown`
+ tests/annotations/mypy_checks.py:46:5: error[type-assertion-failure] Type `str` does not match asserted type `Path`
- tests/annotations/mypy_checks.py:47:5: error[type-assertion-failure] Type `str` does not match asserted type `Unknown`
+ tests/annotations/mypy_checks.py:47:5: error[type-assertion-failure] Type `str` does not match asserted type `Path`
- Found 547 diagnostics
+ Found 550 diagnostics

bokeh (https://github.com/bokeh/bokeh)
- src/bokeh/protocol/__init__.py:131:65: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 692 diagnostics
+ Found 691 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/index_datetime.py:708:54: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 1940 diagnostics
+ Found 1939 diagnostics

scipy (https://github.com/scipy/scipy)
- scipy/stats/_multicomp.py:69:22: warning[possibly-missing-attribute] Attribute `low` may be missing on object of type `@Todo | None`
+ scipy/stats/_multicomp.py:69:22: warning[possibly-missing-attribute] Attribute `low` may be missing on object of type `Unknown | None`
- scipy/stats/_multicomp.py:70:22: warning[possibly-missing-attribute] Attribute `high` may be missing on object of type `@Todo | None`
+ scipy/stats/_multicomp.py:70:22: warning[possibly-missing-attribute] Attribute `high` may be missing on object of type `Unknown | None`

pydantic (https://github.com/pydantic/pydantic)
+ pydantic/types.py:852:12: error[invalid-return-type] Return type does not match returned value: expected `type[set[HashableItemType@conset]]`, found `<typing.Annotated special form>`
+ pydantic/types.py:868:12: error[invalid-return-type] Return type does not match returned value: expected `type[frozenset[HashableItemType@confrozenset]]`, found `<typing.Annotated special form>`
+ pydantic/types.py:903:12: error[invalid-return-type] Return type does not match returned value: expected `type[list[AnyItemType@conlist]]`, found `<typing.Annotated special form>`
- Found 6708 diagnostics
+ Found 6711 diagnostics

No memory usage changes detected ✅

@oconnor663
Copy link
Contributor Author

CI is green. Looking at some of the new diagnostics. For example in werkzeug:

    parameter_storage_class: type[MultiDict[str, t.Any]] = ImmutableMultiDict

It seems like that's a true positive, and that the annotation should be type[MultiDict]? (Alternatively the assigned value could be ImmutableMultiDict[str, t.Any, but I think they want an actual class there? Not totally sure.)

@carljm
Copy link
Contributor

carljm commented Nov 21, 2025

Hmmm, no, I think those ecosystem results might suggest we need to make an adjustment here... and I might have been wrong on the original question about the failing test line. I think we need to default-specialize in that assignment and consider the class object ImmutableMultiDict to have the type type[ImmutableMultiDict[Unknown, Unknown]], which is assignable to type[MultiDict[str, Any]].

What's not totally clear to me is at which level we should do that default specialization. Should we do it on the name load? At the assignability check?

@carljm
Copy link
Contributor

carljm commented Nov 21, 2025

The conformance suite results are showing a similar problem. I think the use of assert_type in those tests is a bit questionable, but it's showing the same problem -- that we aren't default-specializing a generic class literal when the class object is mentioned.

@oconnor663
Copy link
Contributor Author

At the risk of biting off more than I can chew, do we want to revisit this:

            // A non-generic class is never equivalent to a generic class.
            // Two non-generic classes are only equivalent if they are equal (handled above).
            (ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => {
                ConstraintSet::from(false)
            }

I'll try it just for kicks...

@carljm
Copy link
Contributor

carljm commented Nov 22, 2025

No, I don't think that's the right location for the fix.

I think the problem is that we sometimes use ClassType::NonGeneric to represent an unspecialized generic class, and I think we should never(?) do that. ClassType::NonGeneric should always be an actual non-generic class type, and ClassType::Generic should always be a specialized generic class type. Because a true "class type" for a generic class must always be specialized, even if only default-specialized.

We have Type::ClassLiteral to represent a class literal object (non-generic or generic, but never specialized), so that when you say MyGenericClass[X] (an explicit specialization of MyGenericClass) or MyGenericClass(X) (a constructor call of MyGenericClass for which we will infer a specialization), we have some type for MyGenericClass in that expression that knows it isn't specialized already. So we can't just universally make all name-loads of MyGenericClass eagerly default-specialize themselves. But Type::ClassLiteral doesn't wrap ClassType, it directly wraps ClassLiteral, so I think that's still consistent with the idea that we should avoid ever constructing a ClassType that attempts to represent an unspecialized generic class.

I think maybe the narrow version of the fix here would be located at https://github.com/astral-sh/ruff/blob/main/crates/ty_python_semantic/src/types.rs#L2381 -- that is at least one place where we are clearly constructing a ClassType::NonGeneric around an arbitrary ClassLiteral, with no regard for whether it is actually generic or not. Instead I think we should transform our ClassLiteral into a ClassType using a ClassType constructor (which AFAICS doesn't currently exist on ClassType, but I think should) which checks if the ClassLiteral is generic, and if so default-specializes it and constructs a ClassType::Generic around it, only otherwise constructing a ClassType::NonGeneric.

This fix is essentially saying that we consider Type::ClassLiteral(Foo) to be a subtype of Type::SubclassOf(<default specialization of Foo>) -- it's equivalent to the default specialization of Foo itself, excluding subclasses. I think this is a reasonable way to implement the expectation that any mention of Foo which isn't an explicit specialization or a constructor call, should be considered the default specialization.

I suspect that fix may be enough to get the werkzeug example passing, and (depending what the rest of the ecosystem looks like) may be enough to make this landable.

The next follow-up (which could be interesting to try here, in case it just works smoothly, but if it reveals more hard problems may be better deferred) would be to actually add some assertions in ClassType which enforce the invariant that we never make a ClassType::NonGeneric around a generic ClassLiteral.

The next thing which we'd want to look at soon is fixing those conformance suite results. I think we may want to actually make an upstream pull request to the conformance suite to adjust those tests, because I think the way they are written is over-specified. They are asserting that the expression Foo (where Foo is a generic class) must have precisely the type type[Foo[Unknown]] (or whatever the default specialization of Foo might be). I think this is over-prescriptive, because it is valid for us to infer a narrower type here. What the conformance suite really should be asserting here is just assignability behavior, not precise types.

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.

support type[...] where ... is a generic alias

4 participants