Skip to content

[ty] Validate metaclass __new__/__prepare__/__init__, and meta-metaclass __call__, as well as __init_subclass__#24550

Draft
AlexWaygood wants to merge 1 commit intomainfrom
alex/metaclass-new-prepare
Draft

[ty] Validate metaclass __new__/__prepare__/__init__, and meta-metaclass __call__, as well as __init_subclass__#24550
AlexWaygood wants to merge 1 commit intomainfrom
alex/metaclass-new-prepare

Conversation

@AlexWaygood
Copy link
Copy Markdown
Member

Summary

Currently, if a class statement has keyword arguments, we verify those keyword arguments against the signature of the class's __init_subclass__ method. But this isn't necessarily correct! If the class has a metaclass that defines a custom __new__, the keywords passed in the class statement might never get to the __init_subclass__ method -- arbitrary keyword arguments could be consumed or injected by the metaclass's __new__ method. If the class has a custom metaclass that overrides __new__, the keyword arguments passed to the class statement should instead be verified against the __new__ method on the metaclass.

This PR fixes that, and a variety of other edge cases, including:

  • What if the metaclass defines __init__ as well as, or instead of, __new__?
  • What if the metaclass defines __init__ and __new__ and the meta-metaclass defines __call__?
  • What if the __prepare__ method of the metaclass doesn't return the namespace type expected by the __new__ method of the metaclass?
  • Etc. etc.

Test Plan

lots of mdtests and snapshots

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Apr 10, 2026
@AlexWaygood AlexWaygood force-pushed the alex/metaclass-new-prepare branch from c950304 to 7244ec4 Compare April 10, 2026 19:17
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Apr 10, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 87.94%. The percentage of expected errors that received a diagnostic held steady at 83.36%. The number of fully passing files held steady at 79/133.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Apr 10, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 716.79MB 716.99MB +0.03% (207.91kB)
sphinx 262.80MB 262.88MB +0.03% (86.95kB)
trio 117.66MB 117.70MB +0.04% (42.83kB)
flake8 47.94MB 47.97MB +0.06% (29.72kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
infer_scope_types_impl 54.80MB 54.92MB +0.22% (122.39kB)
Type<'db>::apply_specialization_::interned_arguments 2.99MB 3.02MB +0.75% (23.12kB)
Type<'db>::apply_specialization_ 3.70MB 3.72MB +0.47% (17.78kB)
StringLiteralType 5.51MB 5.52MB +0.23% (13.14kB)
TupleType 740.86kB 749.02kB +1.10% (8.16kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 10.07MB 10.06MB -0.07% (7.00kB)
StaticClassLiteral<'db>::implicit_attribute_inner_::interned_arguments 5.37MB 5.37MB -0.11% (6.00kB)
infer_deferred_types 14.59MB 14.59MB +0.03% (4.72kB)
Type<'db>::class_member_with_policy_ 17.62MB 17.62MB +0.02% (3.89kB)
Type<'db>::member_lookup_with_policy_ 17.24MB 17.25MB +0.02% (3.21kB)
infer_definition_types 90.63MB 90.63MB +0.00% (3.20kB)
Type<'db>::class_member_with_policy_::interned_arguments 9.78MB 9.78MB +0.02% (2.44kB)
Type<'db>::try_call_dunder_get_ 10.59MB 10.60MB +0.02% (2.25kB)
FunctionType<'db>::signature_ 4.08MB 4.09MB +0.05% (2.07kB)
Type<'db>::member_lookup_with_policy_::interned_arguments 5.94MB 5.94MB +0.03% (1.83kB)
... 35 more

sphinx

Name Old New Diff Outcome
infer_scope_types_impl 15.45MB 15.48MB +0.20% (32.06kB)
Type<'db>::class_member_with_policy_ 7.63MB 7.63MB +0.08% (6.59kB)
Type<'db>::member_lookup_with_policy_ 6.86MB 6.86MB +0.09% (6.47kB)
infer_deferred_types 5.53MB 5.54MB +0.10% (5.93kB)
Type<'db>::apply_specialization_::interned_arguments 1.44MB 1.45MB +0.33% (4.84kB)
Type<'db>::class_member_with_policy_::interned_arguments 4.03MB 4.03MB +0.10% (3.96kB)
Type<'db>::apply_specialization_ 1.63MB 1.64MB +0.22% (3.71kB)
Type<'db>::member_lookup_with_policy_::interned_arguments 2.67MB 2.67MB +0.12% (3.35kB)
infer_definition_types 23.76MB 23.76MB +0.01% (3.20kB)
TupleType 563.28kB 565.89kB +0.46% (2.61kB)
FunctionType<'db>::signature_ 2.27MB 2.27MB +0.11% (2.56kB)
Type<'db>::try_call_dunder_get_ 4.88MB 4.88MB +0.04% (2.25kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 2.36MB 2.36MB -0.05% (1.20kB)
StaticClassLiteral<'db>::implicit_attribute_inner_::interned_arguments 1.93MB 1.93MB -0.05% (1.03kB)
place_by_id 1.37MB 1.37MB +0.07% (1000.00B)
... 33 more

trio

Name Old New Diff Outcome
infer_scope_types_impl 4.75MB 4.77MB +0.30% (14.78kB)
infer_deferred_types 2.34MB 2.35MB +0.25% (5.93kB)
Type<'db>::apply_specialization_::interned_arguments 642.89kB 647.66kB +0.74% (4.77kB)
Type<'db>::apply_specialization_ 718.34kB 722.05kB +0.52% (3.71kB)
Type<'db>::class_member_with_policy_ 2.05MB 2.05MB +0.16% (3.29kB)
StaticClassLiteral<'db>::try_mro_ 822.38kB 819.25kB -0.38% (3.13kB)
FunctionType<'db>::signature_ 1.07MB 1.07MB +0.23% (2.56kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 747.74kB 745.33kB -0.32% (2.41kB)
StringLiteralType 525.56kB 527.90kB +0.45% (2.34kB)
is_redundant_with_impl 438.73kB 436.41kB -0.53% (2.32kB)
Type<'db>::try_call_dunder_get_ 1.34MB 1.34MB +0.16% (2.25kB)
Type<'db>::class_member_with_policy_::interned_arguments 1.13MB 1.13MB +0.19% (2.23kB)
infer_definition_types 7.61MB 7.61MB +0.03% (2.11kB)
StaticClassLiteral<'db>::implicit_attribute_inner_::interned_arguments 603.19kB 601.12kB -0.34% (2.06kB)
Type<'db>::member_lookup_with_policy_ 1.95MB 1.95MB +0.10% (2.05kB)
... 36 more

flake8

Name Old New Diff Outcome
infer_deferred_types 693.74kB 698.50kB +0.69% (4.75kB)
infer_definition_types 1.87MB 1.87MB +0.18% (3.35kB)
FunctionType<'db>::signature_ 362.48kB 364.84kB +0.65% (2.36kB)
Type<'db>::class_member_with_policy_ 573.53kB 575.23kB +0.30% (1.70kB)
infer_scope_types_impl 989.65kB 991.28kB +0.16% (1.63kB)
function_known_decorators 319.14kB 320.50kB +0.43% (1.36kB)
Type<'db>::try_call_dunder_get_ 368.19kB 369.54kB +0.37% (1.35kB)
Type<'db>::class_member_with_policy_::interned_arguments 312.00kB 313.32kB +0.42% (1.32kB)
FunctionType 440.62kB 441.72kB +0.25% (1.09kB)
place_by_id 143.38kB 144.35kB +0.67% (988.00B)
Type<'db>::apply_specialization_::interned_arguments 201.09kB 202.03kB +0.47% (960.00B)
Type<'db>::member_lookup_with_policy_ 553.20kB 554.04kB +0.15% (860.00B)
place_by_id::interned_arguments 106.80kB 107.51kB +0.66% (720.00B)
Type<'db>::apply_specialization_ 210.35kB 211.05kB +0.33% (720.00B)
OverloadLiteral 120.83kB 121.38kB +0.45% (560.00B)
... 38 more

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Apr 10, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 45 0 0
too-many-positional-arguments 42 0 0
unknown-argument 6 0 0
Total 93 0 0
Raw diff (93 changes)
hydpy (https://github.com/hydpy-dev/hydpy)
+ hydpy/auxs/iuhtools.py:129:10 error[invalid-argument-type] Argument to constructor `MetaIUH.__new__` is incorrect: Expected `tuple[type[Any]]`, found `tuple[()]`

manticore (https://github.com/trailofbits/manticore)
+ server/manticore_server/ManticoreServer_pb2.pyi:151:19 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["HookType"]`
+ server/manticore_server/ManticoreServer_pb2.pyi:151:19 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ server/manticore_server/ManticoreServer_pb2.pyi:315:22 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["StateAction"]`
+ server/manticore_server/ManticoreServer_pb2.pyi:315:22 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4

mypy-protobuf (https://github.com/dropbox/mypy-protobuf)
+ test/generated/testproto/nested/nested_pb2.pyi:51:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NestedEnum"]`
+ test/generated/testproto/nested/nested_pb2.pyi:51:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/nested/nested_pb2.pyi:70:26 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NestedEnum2"]`
+ test/generated/testproto/nested/nested_pb2.pyi:70:26 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/readme_enum_pb2.pyi:28:13 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["MyEnum"]`
+ test/generated/testproto/readme_enum_pb2.pyi:28:13 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/test3_pb2.pyi:33:16 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["OuterEnum"]`
+ test/generated/testproto/test3_pb2.pyi:33:16 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/test3_pb2.pyi:69:20 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["InnerEnum"]`
+ test/generated/testproto/test3_pb2.pyi:69:20 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/nested/nested_pb2.pyi:51:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NestedEnum"]`
+ test/generated_async_only/testproto/nested/nested_pb2.pyi:51:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/nested/nested_pb2.pyi:70:26 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NestedEnum2"]`
+ test/generated_async_only/testproto/nested/nested_pb2.pyi:70:26 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/readme_enum_pb2.pyi:28:13 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["MyEnum"]`
+ test/generated_async_only/testproto/readme_enum_pb2.pyi:28:13 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/test3_pb2.pyi:33:16 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["OuterEnum"]`
+ test/generated_async_only/testproto/test3_pb2.pyi:33:16 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/test3_pb2.pyi:69:20 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["InnerEnum"]`
+ test/generated_async_only/testproto/test3_pb2.pyi:69:20 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/nested/nested_pb2.pyi:51:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NestedEnum"]`
+ test/generated_concrete/testproto/nested/nested_pb2.pyi:51:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/nested/nested_pb2.pyi:70:26 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NestedEnum2"]`
+ test/generated_concrete/testproto/nested/nested_pb2.pyi:70:26 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/readme_enum_pb2.pyi:28:13 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["MyEnum"]`
+ test/generated_concrete/testproto/readme_enum_pb2.pyi:28:13 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/test3_pb2.pyi:33:16 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["OuterEnum"]`
+ test/generated_concrete/testproto/test3_pb2.pyi:33:16 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/test3_pb2.pyi:69:20 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["InnerEnum"]`
+ test/generated_concrete/testproto/test3_pb2.pyi:69:20 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/nested/nested_pb2.pyi:51:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NestedEnum"]`
+ test/generated_sync_only/testproto/nested/nested_pb2.pyi:51:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/nested/nested_pb2.pyi:70:26 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NestedEnum2"]`
+ test/generated_sync_only/testproto/nested/nested_pb2.pyi:70:26 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/readme_enum_pb2.pyi:28:13 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["MyEnum"]`
+ test/generated_sync_only/testproto/readme_enum_pb2.pyi:28:13 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/test3_pb2.pyi:33:16 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["OuterEnum"]`
+ test/generated_sync_only/testproto/test3_pb2.pyi:33:16 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/test3_pb2.pyi:69:20 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["InnerEnum"]`
+ test/generated_sync_only/testproto/test3_pb2.pyi:69:20 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/test_pb2.pyi:46:16 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["OuterEnum"]`
+ test/generated/testproto/test_pb2.pyi:46:16 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/test_pb2.pyi:62:22 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NamingConflicts"]`
+ test/generated/testproto/test_pb2.pyi:62:22 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/test_pb2.pyi:92:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["DeprecatedEnum"]`
+ test/generated/testproto/test_pb2.pyi:92:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/test_pb2.pyi:117:20 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["InnerEnum"]`
+ test/generated/testproto/test_pb2.pyi:117:20 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated/testproto/test_pb2.pyi:351:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["_r_finally"]`
+ test/generated/testproto/test_pb2.pyi:351:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/test_pb2.pyi:46:16 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["OuterEnum"]`
+ test/generated_async_only/testproto/test_pb2.pyi:46:16 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/test_pb2.pyi:62:22 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NamingConflicts"]`
+ test/generated_async_only/testproto/test_pb2.pyi:62:22 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/test_pb2.pyi:92:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["DeprecatedEnum"]`
+ test/generated_async_only/testproto/test_pb2.pyi:92:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/test_pb2.pyi:117:20 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["InnerEnum"]`
+ test/generated_async_only/testproto/test_pb2.pyi:117:20 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_async_only/testproto/test_pb2.pyi:351:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["_r_finally"]`
+ test/generated_async_only/testproto/test_pb2.pyi:351:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/test_pb2.pyi:46:16 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["OuterEnum"]`
+ test/generated_concrete/testproto/test_pb2.pyi:46:16 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/test_pb2.pyi:62:22 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NamingConflicts"]`
+ test/generated_concrete/testproto/test_pb2.pyi:62:22 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/test_pb2.pyi:92:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["DeprecatedEnum"]`
+ test/generated_concrete/testproto/test_pb2.pyi:92:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/test_pb2.pyi:117:20 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["InnerEnum"]`
+ test/generated_concrete/testproto/test_pb2.pyi:117:20 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_concrete/testproto/test_pb2.pyi:351:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["_r_finally"]`
+ test/generated_concrete/testproto/test_pb2.pyi:351:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/test_pb2.pyi:46:16 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["OuterEnum"]`
+ test/generated_sync_only/testproto/test_pb2.pyi:46:16 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/test_pb2.pyi:62:22 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["NamingConflicts"]`
+ test/generated_sync_only/testproto/test_pb2.pyi:62:22 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/test_pb2.pyi:92:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["DeprecatedEnum"]`
+ test/generated_sync_only/testproto/test_pb2.pyi:92:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/test_pb2.pyi:117:20 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["InnerEnum"]`
+ test/generated_sync_only/testproto/test_pb2.pyi:117:20 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4
+ test/generated_sync_only/testproto/test_pb2.pyi:351:21 error[invalid-argument-type] Argument to `_EnumTypeWrapper.__init__` is incorrect: Expected `EnumDescriptor`, found `Literal["_r_finally"]`
+ test/generated_sync_only/testproto/test_pb2.pyi:351:21 error[too-many-positional-arguments] Too many positional arguments to `_EnumTypeWrapper.__init__`: expected 2, got 4

spack (https://github.com/spack/spack)
+ lib/spack/spack/vendor/typing_extensions.py:2206:39 error[unknown-argument] Argument `_root` does not match any known parameter of function `object.__init_subclass__`
+ lib/spack/spack/vendor/typing_extensions.py:1649:47 error[unknown-argument] Argument `_root` does not match any known parameter of function `object.__init_subclass__`
+ lib/spack/spack/vendor/typing_extensions.py:1953:49 error[unknown-argument] Argument `_root` does not match any known parameter of function `object.__init_subclass__`
+ lib/spack/spack/vendor/typing_extensions.py:2066:47 error[unknown-argument] Argument `_root` does not match any known parameter of function `object.__init_subclass__`
+ lib/spack/spack/vendor/typing_extensions.py:2453:46 error[unknown-argument] Argument `_root` does not match any known parameter of function `object.__init_subclass__`
+ lib/spack/spack/vendor/typing_extensions.py:2597:44 error[unknown-argument] Argument `_root` does not match any known parameter of function `object.__init_subclass__`

strawberry (https://github.com/strawberry-graphql/strawberry)
+ strawberry/types/auto.py:69:21 error[invalid-argument-type] Argument to `StrawberryAutoMeta.__init__` is incorrect: Expected `str`, found `dict[str, Any]`
+ strawberry/types/auto.py:69:21 error[invalid-argument-type] Argument to `StrawberryAutoMeta.__init__` is incorrect: Expected `str`, found `tuple[()]`

Full report with detailed diff (timing results)

@AlexWaygood
Copy link
Copy Markdown
Member Author

The "regressions" in the conformance suite are all due to invalidly defined classes in the conformance tests that would fail to be created at runtime. I've filed python/typing#2258 to fix that.

@AlexWaygood
Copy link
Copy Markdown
Member Author

Ecosystem analysis

Lots of ddtrace-py diagnostics! These all look to be because they have lots of classes that use EnvMeta as a metaclass. EnvMeta.__new__ says that it expects an argument of type tuple[type] as the bases argument, which I'm almost certain is an error in that parameter annotation -- it should surely be tuple[type, ...].

Exactly the same issue is also the root cause of the hydpy diagnostics: https://github.com/hydpy-dev/hydpy/blob/b5a1b04a5f07346c7ee4d6556894a75723e69d9a/hydpy/auxs/iuhtools.py#L107-L116.

The manticore diagnostics appear to be due to a questionable __init__ signature in typeshed's protobuf stubs here, and I think all the diagnostics in mypy-protobuf are for the same reason.

spack is attempting to subclass typing._SpecialForm, which does have __init_subclass__ at runtime, but typeshed doesn't acknowledge this. The reason is that you're really not supposed to subclass typing._SpecialForm unless you're typing_extensions or typing; I think this is an acceptable diagnostic.

The strawberry diagnostic is also due to a mis-annotated __init__ method on a metaclass.

These all seem like true positives, therefore; I think the only reason that there are so many is that no other type checker has tried to properly emulate the runtime semantics here before, so these have all gone unnoticed by prior type checkers.

@AlexWaygood AlexWaygood marked this pull request as ready for review April 10, 2026 20:03
@AlexWaygood AlexWaygood assigned carljm and unassigned sharkdp Apr 10, 2026
@AlexWaygood AlexWaygood force-pushed the alex/init-subclass-none branch from 9708615 to 52a07c9 Compare April 10, 2026 22:00
@AlexWaygood AlexWaygood force-pushed the alex/metaclass-new-prepare branch 2 times, most recently from 9fbfb17 to b958a6f Compare April 10, 2026 22:06
@AlexWaygood
Copy link
Copy Markdown
Member Author

The "regressions" in the conformance suite are all due to invalidly defined classes in the conformance tests that would fail to be created at runtime. I've filed python/typing#2258 to fix that.

(All gone now that we've updated our conformance-suite pin to a commit that includes python/typing@4c02514)

@AlexWaygood
Copy link
Copy Markdown
Member Author

The diagnostics on this branch aren't great, but they'll be greatly improved by a combination of #24560 and #24565

@AlexWaygood AlexWaygood force-pushed the alex/init-subclass-none branch from 447364e to 8075ab1 Compare April 12, 2026 13:34
Base automatically changed from alex/init-subclass-none to main April 12, 2026 13:38
@AlexWaygood AlexWaygood marked this pull request as draft April 12, 2026 13:44
@AlexWaygood AlexWaygood force-pushed the alex/metaclass-new-prepare branch 2 times, most recently from fb90234 to 0c1c88d Compare April 12, 2026 14:24
@AlexWaygood AlexWaygood marked this pull request as ready for review April 12, 2026 14:26
@AlexWaygood AlexWaygood force-pushed the alex/metaclass-new-prepare branch 5 times, most recently from 789e561 to bdf657c Compare April 12, 2026 16:21
@Viicos
Copy link
Copy Markdown
Contributor

Viicos commented Apr 13, 2026

Hi @AlexWaygood,

thanks for the work on this. Similar to the spec about constructors, I'm wondering if it would make sense to incorporate part of the logic here into the spec (things like __init_subclass__() isn't checked if an explicit __new__() is defined, etc).

In Pydantic, we allow class arguments to be used (e.g. class Model(BaseModel, strict=True): ...), and we currently rely on __init_subclass__() for such arguments to be type-checked. However, we also define an explicit metaclass __new__(). As things aren't specified, we currently rely on what happens to be supported by pyright (which is inconsistent, so properly specifying the behavior would help).

Does this make sense? I'd be happy to take a stab at it, opening a DPO discussion first.

@AlexWaygood
Copy link
Copy Markdown
Member Author

Thanks @Viicos! Yes, it could definitely make sense to standardise some of the rules here.

Can you paste some links to some of these classes that have __init_subclass__ methods but also have __new__ defined on their metaclasses? Pydantic isn't showing up in the ecosystem report on this PR, so I have to assume that this PR won't cause any new false positives for pydantic or users of pydantic. But it's possible that it might introduce new false negatives -- I'd be curious to take a look at what pydantic's code looks like in practice here.

@Viicos
Copy link
Copy Markdown
Contributor

Viicos commented Apr 13, 2026

So in our case the metaclass' __new__() is defined here 1:

@dataclass_transform(kw_only_default=True, field_specifiers=(PydanticModelField, PydanticModelPrivateAttr, NoInitField))
class ModelMetaclass(ABCMeta):
    def __new__(
        mcs,
        cls_name: str,
        bases: tuple[type[Any], ...],
        namespace: dict[str, Any],
        __pydantic_generic_metadata__: PydanticGenericMetadata | None = None,
        __pydantic_reset_parent_namespace__: bool = True,
        _create_model_module: str | None = None,
        **kwargs: Any,
    ) -> type:

And the __init_sublcass__() is defined here (and is currently defined for type checking purposes):

if TYPE_CHECKING:
    def __init_subclass__(cls, **kwargs: Unpack[ConfigDict]):
        ...

The idea for us would be to remove the __init_subclass__() type definition, and rely on the __new__() directly.

Footnotes

  1. I'll note that our -> type return type is probably wrong, and a type var should be used, as it is in typeshed for type.__new__().

@AlexWaygood
Copy link
Copy Markdown
Member Author

AlexWaygood commented Apr 13, 2026

I see. So would it be correct to say that:

  1. if all type checkers had the behaviour I have in this PR, it would be strictly better for you, because you'd be able to get rid of the fake __init_subclass__ definition in the if TYPE_CHECKING block?
  2. but if only ty has this behaviour, it's probably worse for you, because you now have to worry about the annotations of your metaclass __new__ method as well as the annotations on your __init_subclass__ method? (But maybe that's just a matter of changing your **kwargs argument so that it's annotated as **kwargs: Unpack[ConfigDict] rather than **kwargs: Any? I haven't looked too closely at what your metaclass __new__ method does yet, to be clear.)

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

Nice!

Comment on lines +745 to +747
class Valid(Base, meta_arg=5, metaclass=Meta): ...
class MissingArg(Base, metaclass=Meta): ... # error: [missing-argument]
class InvalidType(Base, meta_arg="foo", metaclass=Meta): ... # error: [invalid-argument-type]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth also testing here that sub_arg is an unknown parameter?

Comment thread crates/ty_python_semantic/resources/mdtest/call/methods.md
def __prepare__(mcs, name: str, bases: tuple[type, ...], *, prep_arg: int = 0, **kwargs: Any) -> dict[str, Any]:
return {}

def __new__(mcs, name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because this signature of __new__ accepts everything __prepare__ does, this test doesn't establish that we are validating against both signatures.

Comment thread crates/ty_python_semantic/resources/mdtest/call/methods.md
Comment thread crates/ty_python_semantic/resources/mdtest/call/methods.md
12 | return super().__new__(mcs, name, bases, namespace)
13 |
14 | class Foo(metaclass=Meta): ... # error: [invalid-argument-type]
| ^^^^^^^^^^^^^^^^ Expected `MyNamespace`, found `dict[str, Any]`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess ideally we'd point to the return annotation on __new__ to explain where dict[str, Any] came from?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think you mean the return annotation on __prepare__? And my answer would be "yes, ideally", but I think figuring out exactly how to attach that diagnostic from deep inside our call-binding machinery might be one of those "I'll need a research team and five years" problems (https://xkcd.com/1425/)

Comment on lines +1226 to +1240
// If the `metaclass=` keyword argument is not passed an instance of `type`,
// the runtime just calls it as-is without trying to resolve the appropriate metaclass
// with respect to the class's base classes. So, just use the `metaclass=` argument directly
// in that case. If the `metaclass=` argument was passed an instance of `type`, however,
// the class's actual metaclass won't necessarily be the `metaclass=` argument: if you pass
// `metaclass=object`, the class's actual metaclass will be `type`, because all classes have
// `object` as a supertype and the metaclass of `object` is `type`, and `object`/`type` have
// a subclass relationship.
let metaclass = if let Some(explicit_metaclass) = explicit_metaclass
&& !explicit_metaclass.is_subtype_of(db, KnownClass::Type.to_instance(db))
{
explicit_metaclass
} else {
class.metaclass(db)
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like this logic is somewhat iffy and at least deserves a TODO. It is also, I think, the cause of the regression in handling of metaclass=Any, because typing.Any is a class (thus an instance of type), but it is not something that try_metaclass is able to recognize as a metaclass, so it just gets reduced to Unknown.

I think that regression illustrates the issue with this logic: the mismatch between "metaclasses that are actually recognized by try_metaclass" vs "things we consider a subtype of type" leaves a gap where invalid metaclasses can silently become Unknown and not be validated at all here.

I suspect that instead of implementing our own separate logic here, we should maybe be calling try_metaclass directly, and use explicit_metaclass if it returns an error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I suspect that instead of implementing our own separate logic here, we should maybe be calling try_metaclass directly, and use explicit_metaclass if it returns an error?

That's a great suggestion. Thanks!

Comment on lines +1268 to +1272
} else if !metaclass.is_subtype_of(db, KnownClass::Type.to_subclass_of(db)) {
// `ClassLiteral::metaclass()` doesn't give an accurate result currently if the metaclass
// was inherited from a superclass and is not an instance of `type` (which is anyway very rare),
// so we don't check either `__init_subclass__` or the call to the metaclass in that case right now.
(explicit_metaclass.is_some(), false)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be easier to understand the logic if we clarified in what way ClassLiteral::metaclass() gives wrong results today in the mentioned case. Does it error and so .metaclass() just returns type[Unknown]?

Given that we actually perform our metaclass-call checks below against metaclass, not explicit_metaclass, it feels like we are assuming here that if metaclass is not a subclass of type but explicit_metaclass.is_some(), that means we fell into the "unrecognizable metaclass" case above, and metaclass == explicit_metaclass? But I'm not sure if that's the intent or not... can we preserve more state from the check above and do less re-checking / assuming here?

This feels related to the comment above -- in general it seems like we're doing a fair amount of "working around" try_metaclass, and it would be a lot clearer if it could make the necessary distinctions and centralize them in one place.

Comment on lines +1296 to +1299
// We pass in a tuple where all elements are `type` rather than passing in the actual bases,
// because `Generic[T]` is not actually an instance of `type`, but most metaclass `__new__`
// and `__prepare__` methods are annotated as accepting `tuple[type, ...]`, which leads to
// too many false-positive errors.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This workaround can also cause false positives, though I assume much less likely (or you'd have seen it in the ecosystem):

from typing import Any

class A: ...

class Meta(type):
    def __new__(
        mcls,
        name: str,
        bases: tuple[type[A]],
        namespace: dict[str, Any],
    ):
        return super().__new__(mcls, name, bases, namespace)

class Good(A, metaclass=Meta): ...

This should be fine, but it fails because we replace the actual bases with type.

Maybe this is an acceptable limitation, and we can just document it with a TODO?

Or it seems like we might be able to just recognize the cases (Protocol, Generic) that run into this, and special-case them? (Mirroring the __mro_entries__ magic at runtime.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

__mro_entries__ could return a tuple of length >=1, so there's sort-of no hope that we can actually model all this correctly. (You know, we could try looking up the return type of __mro_entries__ for base classes that are not instances of type, but I really doubt that anybody using __mro_entries__ is precisely annotating the return type in the hope that a type checker will understand it.)

>>> class Foo: ...
... 
>>> class Bar: ...
... 
>>> class Baz:
...     def __mro_entries__(self, *args, **kwargs):
...         return Foo, Bar
...         
>>> class Spam(Baz()): ...
... 
>>> Spam.__bases__
(<class '__main__.Foo'>, <class '__main__.Bar'>)

but if all items in the bases list are instances of type, then we know that __mro_entries__ will never be called on any of those items... so in that instance, it should be safe to pass in the bases tuple as-is...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, sorry, I wasn't clear here. I didn't mean trying to fully model __mro_entries__ behavior, just recognize the commonly used classes that we know have it, and special-case them enough to make it work. The tuple[Any, ...] solution we discussed in-person should eliminate false positives, I think?

Comment thread crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs Outdated
@Viicos
Copy link
Copy Markdown
Contributor

Viicos commented Apr 14, 2026

  1. if all type checkers had the behaviour I have in this PR, it would be strictly better for you, because you'd be able to get rid of the fake __init_subclass__ definition in the if TYPE_CHECKING block?

Essentially yes, our policy is to comply to the spec and support type checkers that comply to the spec

2. but if only ty has this behaviour, it's probably worse for you, because you now have to worry about the annotations of your metaclass __new__ method as well as the annotations on your __init_subclass__ method? (But maybe that's just a matter of changing your **kwargs argument so that it's annotated as **kwargs: Unpack[ConfigDict] rather than **kwargs: Any? I haven't looked too closely at what your metaclass __new__ method does yet, to be clear.)

Thankfully, even if Pyright's behavior is inconsistent when it comes to typing extra kwargs to the metaclass' __new__(), it does support the Unpack[ConfigDict] case. But even if this wasn't the case, I wouldn't be too worried because this is kind of an unusual pattern to get typed checked. So I would say if this were to be spec'd out, our aim would be to follow what the spec would say, with hopes that pyright will comply to it at some point.

@AlexWaygood AlexWaygood force-pushed the alex/metaclass-new-prepare branch from 17ef206 to 52e0fec Compare April 16, 2026 18:44
@AlexWaygood AlexWaygood changed the base branch from main to alex-carl/dataclass_transform April 16, 2026 18:45
@AlexWaygood AlexWaygood reopened this Apr 16, 2026
@astral-sh-bot astral-sh-bot bot requested a review from carljm April 16, 2026 18:45
@AlexWaygood AlexWaygood marked this pull request as draft April 16, 2026 18:45
@AlexWaygood AlexWaygood force-pushed the alex/metaclass-new-prepare branch 2 times, most recently from a4b2932 to fbe0562 Compare April 16, 2026 19:06
Base automatically changed from alex-carl/dataclass_transform to main April 16, 2026 19:29
…metaclass `__call__`, as well as `__init_subclass__`
@AlexWaygood AlexWaygood force-pushed the alex/metaclass-new-prepare branch from fbe0562 to 2d243ac Compare April 16, 2026 19:29
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.

4 participants