From 3d9e125ac9c833f68350add806a59d1733b271a8 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:31:22 +0200 Subject: [PATCH 1/5] Add section about metaclass constructors --- docs/conf.py | 5 +- docs/spec/constructors.rst | 288 +++++++++++++++++++++++++++++++++++++ 2 files changed, 292 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index f16401bae..cd155fffa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,4 +52,7 @@ html_static_path = [] extensions = ['sphinx.ext.intersphinx'] -intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'py315': ('https://docs.python.org/3.15', None), +} diff --git a/docs/spec/constructors.rst b/docs/spec/constructors.rst index c95ecc488..c5aa10b1e 100644 --- a/docs/spec/constructors.rst +++ b/docs/spec/constructors.rst @@ -3,6 +3,8 @@ Constructors Calls to constructors require special handling within type checkers. +.. _`constructor-calls`: + Constructor Calls ----------------- @@ -488,3 +490,289 @@ callable. def __init__[V](self, x: T, y: list[V], z: V) -> None: ... reveal_type(accepts_callable(MyClass)) # ``def [T, V] (x: T, y: list[V], z: V) -> MyClass[T]`` + + +Metaclass Constructors +---------------------- + +A class object is itself an instance of its :term:`python:metaclass`, so the +creation of a class is also a constructor call, one made on the metaclass. While +the sections above describe how a metaclass participates in the construction of +*instances* of a class, the following sections describe the construction of class objects +themselves. + +A metaclass constructor is invoked in one of two ways: + +1. Directly, by calling the metaclass with a class name, a tuple of base + classes, and a namespace dictionary (for example, + ``Meta(name, bases, namespace)``), optionally along with additional keyword + arguments. +2. Implicitly, by a :keyword:`python:class` statement, which assembles these + three arguments from the statement and the class body and then calls the metaclass. + +In both cases, the metaclass call should be evaluated using the same rules +described in the sections above: the :meth:`!__call__` method of the metaclass's +own metaclass (typically :meth:`!type.__call__`) is invoked, which in turn calls +the :meth:`!__new__` and :meth:`!__init__` methods of the metaclass. These methods are +typically inherited from :class:`type`, whose type definitions require special +handling by type checkers, as described below. + +The following example illustrates these rules applied to metaclass calls: + + :: + + class MetaMeta(type): + def __call__(cls, *args, **kwargs) -> Never: + raise TypeError("Classes cannot be created with this metaclass") + + class Meta1(type, metaclass=MetaMeta): + pass + + # The __call__() method of the metaclass's own metaclass is evaluated first: + assert_type(Meta1("A", (), {}), Never) + + class Meta2(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + # Then, the __new__ and __init__ methods of the metaclass are evaluated: + Meta2("B", (), {}, key=1) # OK, evaluates to an instance of Meta2 + Meta2("B", (), {}) # Type error: missing argument "key" + + class Meta3(type): + def __new__( + mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] + ) -> int: + return 0 + + # Not evaluated, as __new__ does not return an instance of Meta3: + def __init__(cls, x: str) -> None: + pass + + assert_type(Meta3("C", (), {}), int) + + +The ``type`` Constructor +------------------------ + +In addition to being the default metaclass, :class:`type` serves a second purpose: +when called with a single argument, it returns the type of that argument rather than +creating a new class. :class:`type` therefore supports two distinct call forms, +which are distinguished by the number of positional arguments: + +* ``type(obj, /)`` returns the class of ``obj``. +* ``type(name, bases, dict, /, **kwds)`` creates and returns a new class. + +These two forms are typically declared as overloads of the :meth:`!__new__` and +:meth:`!__init__` methods in the type definition of :class:`type`. Both forms require +special-case handling by type checkers. + +Although the single-argument form is typically declared with a return type of +``type``, type checkers should special-case this form and evaluate its result +as ``type[T]``, where ``T`` is the type of the argument. + + :: + + def func(x: int, y: int | str) -> None: + assert_type(type(x), type[int]) + assert_type(type(y), type[int] | type[str]) + +At runtime, the single-argument form applies only when the class being called +is :class:`type` itself, and is not inherited by metaclasses: a single-argument call +to a subclass of :class:`type` raises a :exc:`TypeError`. + + :: + + class Meta(type): + pass + + assert_type(type(1), type[int]) # OK, uses the single-argument form + Meta(1) # Type error: single-argument form does not apply to subclasses + + Meta("A", (), {}) # OK, uses the three-argument form + +This special-casing applies only to the :meth:`!__new__` and :meth:`!__init__` +methods inherited from :class:`type`. If a metaclass defines its own :meth:`!__new__` +method that accepts a single argument, calls to it should be evaluated +using the rules for regular constructor calls described earlier in this +chapter. + +The evaluated return type of the three-argument form is an instance of the +metaclass being called, consistent with the return type definition of +:meth:`!type.__new__`. Type checkers may infer a more precise type for the returned +class object, for example, one equivalent to a class defined by a :keyword:`class` +statement with the given name, base classes, and namespace. + + +Class Statements +---------------- + +When a :keyword:`class` statement is executed, the runtime performs the following +steps to create the new class object (see :ref:`python:metaclasses`): + +1. The metaclass is determined. If a ``metaclass`` keyword argument is present + in the class statement's argument list, it is used as a candidate; + otherwise, :class:`type` is. The most derived metaclass among the candidate and + the metaclasses of all base classes is selected. If no candidate is a + (non-strict) subclass of all of the others, a :exc:`TypeError` is raised + (see :ref:`py315:metaclass-determination`). +2. The class namespace is prepared. If the metaclass has a :attr:`!__prepare__` + attribute, it is called as ``Meta.__prepare__(name, bases, **kwds)``, and + its result is used as the namespace object. +3. The class body is executed within this namespace. +4. The metaclass is called as ``Meta(name, bases, namespace, **kwds)``, where + ``kwds`` consists of the keyword arguments that appear in the class + statement's argument list, excluding ``metaclass`` itself. + +Type checkers may report an error for a class statement whose base classes +have incompatible metaclasses. + +Type checkers should validate keyword arguments in a class statement's +argument list (other than ``metaclass``) by evaluating the implied metaclass +call using the constructor call rules described in :ref:`constructor-calls`. + + :: + + class Meta(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + class MyClass1(metaclass=Meta, key=3): # OK + pass + + class MyClass2(metaclass=Meta, key=""): # Type error: wrong type for "key" + pass + + class MyClass3(metaclass=Meta): # Type error: missing argument "key" + pass + + Meta("MyClass4", (), {}, key=3) # OK + Meta("MyClass5", (), {}, key="") # Type error: wrong type for "key" + +Keyword arguments in a direct metaclass call (such as the last two calls in the +example above) require no special handling: they are validated as part of +evaluating the call using the standard constructor call rules. + +Regardless of the evaluated return type of the implied metaclass call, a +:keyword:`class` statement defines a class, and type checkers should evaluate the +type of the bound name accordingly (``type[MyClass1]`` in the example above). +The implied metaclass call is evaluated only for the purpose of validating +its arguments. + +Type checkers may validate the implied call to :attr:`!__prepare__`: + + :: + + class Meta(type): + @classmethod + def __prepare__(mcls, name: str, bases: tuple[type, ...]): # No **kwds + return {} + + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + # The 'key' argument may result in a type checker error: + class MyClass6(metaclass=Meta, key=3): + pass + +The ``metaclass`` argument can also be an arbitrary callable that is not a subclass +of :class:`type`. Support for this pattern is currently unspecified. + + +The ``__init_subclass__()`` Method +---------------------------------- + +:meth:`!type.__new__` invokes the :meth:`~object.__init_subclass__` +method of the parent class (the class that follows the newly created class in +its :term:`python:method resolution order`) passing the newly created class as +``cls`` along with the keyword arguments supplied to the metaclass constructor +(see :ref:`python:class-customization`). + +:meth:`~object.__init_subclass__` is implicitly a class method: it is converted to +a :class:`classmethod` even when it is not explicitly decorated as one, and it is +not called for the class that defines it, only for its subclasses. Type checkers +should treat it as a classmethod if it isn't explicitly defined as one. + +If the metaclass of the class being defined does not define its own +:meth:`!__new__` method (including when no explicit metaclass +is specified), type checkers should validate the keyword arguments in a +class statement's argument list against the :meth:`~object.__init_subclass__` method of +the parent class. + + :: + + class Base: + def __init_subclass__(cls, *, flag: bool = False) -> None: + super().__init_subclass__() + + class MyClass1(Base, flag=True): # OK + pass + + class MyClass2(Base, flag=""): # Type error: wrong type for "flag" + pass + + class MyClass3(Base, other=1): # Type error: Base.__init_subclass__() got an unexpected keyword argument 'other' + pass + + class MyClass4(other=1): # Type error: MyClass4.__init_subclass__() takes no keyword arguments + pass + +A metaclass :meth:`!__init__` method has no effect on this rule: when the +metaclass does not define its own :meth:`!__new__` method, :meth:`!type.__new__` +still forwards the keyword arguments to :meth:`~object.__init_subclass__`, so +the keyword arguments should satisfy both the metaclass :meth:`!__init__` +method (as part of validating the implied metaclass call) and the +:meth:`~object.__init_subclass__` method of the parent class. + + :: + + class MetaInit(type): + def __init__( + cls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ) -> None: + super().__init__(name, bases, namespace) + + # Type error: "key" is accepted by MetaInit.__init__(), but type.__new__() + # forwards it to Base.__init_subclass__(), which does not accept it: + class MyClass5(Base, metaclass=MetaInit, key=1): + pass + +The same forwarding occurs when the metaclass is called directly: +``type("D", (Base,), {}, flag=True)`` passes ``flag`` to +:meth:`!Base.__init_subclass__`. Type checkers may validate keyword +arguments in such calls against the :meth:`~object.__init_subclass__` method of +the parent class when the base classes can be statically determined. + +If the metaclass defines its own :meth:`!__new__` method that accepts keyword +arguments only through a ``**kwargs`` parameter, whether these arguments are +forwarded to :meth:`!type.__new__` (and from there to +:meth:`~object.__init_subclass__`) cannot generally be determined statically. +In this situation, type checkers may additionally validate the keyword +arguments against the :meth:`~object.__init_subclass__` method of the parent +class. From 1ad15232d4244da2f5c5e05e907b0d47027a2023 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:31:37 +0200 Subject: [PATCH 2/5] Add temporary type checker analysis --- metaclass-constructors-checkers.md | 356 +++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 metaclass-constructors-checkers.md diff --git a/metaclass-constructors-checkers.md b/metaclass-constructors-checkers.md new file mode 100644 index 000000000..c4c00c767 --- /dev/null +++ b/metaclass-constructors-checkers.md @@ -0,0 +1,356 @@ +# Metaclass constructors: spec examples vs. type checkers + +This document runs the examples from the [Metaclass Constructors](docs/spec/constructors.rst) +spec sections against CPython and four type checkers, to assess how far current +implementations are from the proposed behavior. + +Environment: + +- Runtime: CPython 3.14.5 +- mypy 2.3.0 (default settings) +- pyright 1.1.411 (default settings) +- ty 0.0.65 (default settings) +- pyrefly 1.1.1 (`--preset default`; the implicit `basic` preset disables + call-shape validation entirely and reports nothing on these examples) + +Each significant line is annotated with a comment of the form: + +``` +# spec: | runtime: | mypy: | pyright: | ty: | pyrefly: +``` + +- `spec:` is what the spec expects a type checker to report: `error` (should), + `may error` (optional), or `ok` (no error). For `assert_type()` lines, `ok` + means the assertion should pass. +- `runtime:` is what CPython does when the statement is executed. Note that a + line can be a type error while running fine (e.g. a wrongly typed keyword + value is not checked at runtime), and vice versa (an `assert_type()` whose + argument raises). +- A checker column says `error` if the checker reports any diagnostic on that + line, `ok` otherwise. + +## Metaclass Constructors (intro example) + +```python +from typing import Any, Never, assert_type + + +class MetaMeta(type): + def __call__(cls, *args, **kwargs) -> Never: + raise TypeError("Classes cannot be created with this metaclass") + + +class Meta1(type, metaclass=MetaMeta): + pass + + +# spec: ok | runtime: TypeError (by design) | mypy: error | pyright: ok | ty: ok | pyrefly: ok +assert_type(Meta1("A", (), {}), Never) + + +class Meta2(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +Meta2("B", (), {}, key=1) + +# spec: error | runtime: TypeError | mypy: error | pyright: error | ty: error | pyrefly: error +# runtime: TypeError: Meta2.__new__() missing 1 required keyword-only argument: 'key' +Meta2("B", (), {}) + + +class Meta3(type): + # spec: ok | mypy: error (rejects an int-returning __new__ at the definition) | pyright: ok | ty: ok | pyrefly: ok + def __new__( + mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] + ) -> int: + return 0 + + def __init__(cls, x: str) -> None: + pass + + +# spec: ok | runtime: ok | mypy: error | pyright: ok | ty: ok | pyrefly: ok +assert_type(Meta3("C", (), {}), int) +``` + +Notes: + +- pyright, ty, and pyrefly all honor the metametaclass `__call__()` returning + `Never` and the `__init__()`-skipping rule when `__new__()` returns `int` + (`reveal_type` confirms `Never` and `int` for ty and pyrefly). mypy evaluates + `Meta1(...)` as `Meta1` and rejects the `Meta3.__new__()` definition outright. +- The direct call missing `key` is flagged by **all four** checkers — direct + metaclass calls go through the standard constructor-call rules everywhere. + +## The `type` Constructor — single-argument form inference + +```python +from typing import assert_type + + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +def func(x: int, y: int | str) -> None: + assert_type(type(x), type[int]) + assert_type(type(y), type[int] | type[str]) +``` + +All four checkers already special-case `type(obj)` to `type[T]` +(pyrefly reveals exactly `type[int]`; ty infers the even more precise class +literal `` — see next example). + +## The `type` Constructor — single-argument form on subclasses + +```python +from typing import assert_type + + +class Meta(type): + pass + + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: error | pyrefly: ok +assert_type(type(1), type[int]) + +# spec: error | runtime: TypeError | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +# runtime: TypeError: type.__new__() takes exactly 3 arguments (1 given) +Meta(1) + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +Meta("A", (), {}) +``` + +Notes: + +- **No checker currently reports `Meta(1)`**: all four inherit the + single-argument overload of `type.__new__()`/`type.__init__()` into the + subclass. +- ty's error on `assert_type(type(1), type[int])` is an artifact of it inferring + the *more precise* class literal `` for `type(1)` and reporting + the mismatch as `assert-type-unspellable-subtype`; the inference itself is + compliant. + +## Class Statements — keyword arguments vs. the metaclass constructor + +```python +from typing import Any + + +class Meta(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +class MyClass1(metaclass=Meta, key=3): + pass + + +# spec: error (wrong type for "key") | runtime: ok | mypy: ok | pyright: error | ty: ok | pyrefly: ok +class MyClass2(metaclass=Meta, key=""): + pass + + +# spec: error (missing "key") | runtime: TypeError | mypy: ok | pyright: error | ty: ok | pyrefly: ok +# runtime: TypeError: Meta.__new__() missing 1 required keyword-only argument: 'key' +class MyClass3(metaclass=Meta): + pass + + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +Meta("MyClass4", (), {}, key=3) + +# spec: error (wrong type for "key") | runtime: ok | mypy: error | pyright: error | ty: error | pyrefly: error +Meta("MyClass5", (), {}, key="") +``` + +Notes: + +- Only pyright validates class-statement keyword arguments against a custom + metaclass `__new__()` today. mypy, ty, and pyrefly all miss `MyClass2` and + `MyClass3` — including the missing-argument case that fails at runtime. +- The equivalent *direct* calls are validated by all four checkers through the + standard constructor-call rules. + +## Class Statements — the implied `__prepare__` call + +```python +from typing import Any + + +class Meta(type): + # all four checkers report an override-incompatibility error at this + # definition (parameter "**kwds" missing vs. type.__prepare__); that check + # is unrelated to validating the implied __prepare__ call below + @classmethod + def __prepare__(mcls, name: str, bases: tuple[type, ...]): # No **kwds + return {} + + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +# spec: may error | runtime: TypeError | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +# runtime: TypeError: Meta.__prepare__() got an unexpected keyword argument 'key' +class MyClass6(metaclass=Meta, key=3): + pass +``` + +No checker validates the implied `__prepare__` call at the class statement +(consistent with the spec's "may"). All four do flag the `__prepare__` +*definition* as an incompatible override of `type.__prepare__()`, which +indirectly catches this class of bug. + +## The `__init_subclass__()` Method — no custom metaclass `__new__()` + +```python +class Base: + def __init_subclass__(cls, *, flag: bool = False) -> None: + super().__init_subclass__() + + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +class MyClass1(Base, flag=True): + pass + + +# spec: error (wrong type for "flag") | runtime: ok | mypy: error | pyright: error | ty: error | pyrefly: ok +class MyClass2(Base, flag=""): + pass + + +# spec: error (unknown argument) | runtime: TypeError | mypy: error | pyright: error | ty: error | pyrefly: ok +# runtime: TypeError: Base.__init_subclass__() got an unexpected keyword argument 'other' +class MyClass3(Base, other=1): + pass + + +# spec: error (object.__init_subclass__ accepts no kwargs) | runtime: TypeError | mypy: error | pyright: error | ty: ok | pyrefly: ok +# runtime: TypeError: MyClass4.__init_subclass__() takes no keyword arguments +class MyClass4(other=1): + pass +``` + +mypy, pyright, and ty implement this rule (ty misses only the +`object.__init_subclass__()` case); pyrefly performs no class-keyword validation. + +## The `__init_subclass__()` Method — metaclass with only a custom `__init__()` + +```python +from typing import Any + + +class Base: + def __init_subclass__(cls, *, flag: bool = False) -> None: + super().__init_subclass__() + + +class MetaInit(type): + def __init__( + cls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ) -> None: + super().__init__(name, bases, namespace) + + +# spec: error | runtime: TypeError | mypy: ok | pyright: error | ty: error | pyrefly: ok +# runtime: TypeError: Base.__init_subclass__() got an unexpected keyword argument 'key' +class MyClass5(Base, metaclass=MetaInit, key=1): + pass +``` + +`key` is accepted by `MetaInit.__init__()`, but `type.__new__()` (not +overridden) still forwards it to `Base.__init_subclass__()`, which rejects it. +pyright and ty report it; mypy and pyrefly do not. + +## The `__init_subclass__()` Method — forwarding through direct calls and `**kwargs` + +```python +from typing import Any + + +class Base: + def __init_subclass__(cls, *, flag: bool = False) -> None: + super().__init_subclass__() + + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +type("D", (Base,), {}, flag=True) + +# spec: may error | runtime: TypeError | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +# runtime: TypeError: Base.__init_subclass__() got an unexpected keyword argument 'other' +type("E", (Base,), {}, other=1) + + +class MetaKwargs(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + **kwargs: Any, + ): + return super().__new__(mcls, name, bases, namespace, **kwargs) + + +# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok +class MyClass7(Base, metaclass=MetaKwargs, flag=True): + pass + + +# spec: may error | runtime: TypeError | mypy: ok | pyright: ok | ty: error | pyrefly: ok +# runtime: TypeError: Base.__init_subclass__() got an unexpected keyword argument 'other' +class MyClass8(Base, metaclass=MetaKwargs, other=1): + pass +``` + +No checker validates `__init_subclass__()` through a *direct* `type(...)` call. +Only ty follows the forwarding through a `**kwargs`-accepting metaclass +`__new__()` in a class statement, a consequence of ty checking +`__init_subclass__()` unconditionally — which also produces false positives +when a strict metaclass *consumes* a keyword argument (e.g. +`class C(Base, metaclass=MetaStrict, key=1)` where `key` never reaches +`__init_subclass__()`). + +## Summary of divergences from the proposed spec text + +| Rule | mypy | pyright | ty | pyrefly | +| --- | --- | --- | --- | --- | +| `type(obj)` evaluates to `type[T]` | yes | yes | yes (more precise) | yes | +| One-argument form rejected on `type` subclasses | no | no | no | no | +| Metametaclass `__call__()` governs metaclass calls | no | yes | yes | yes | +| `__init__()` skipped when metaclass `__new__()` returns non-instance | no | yes | yes | yes | +| Direct metaclass call arguments validated | yes | yes | yes | yes | +| Class-statement kwargs vs. custom metaclass `__new__()` | no | yes | no | no | +| Class-statement kwargs vs. `__init_subclass__()` (should) | yes | yes | mostly | no | +| `__init__()`-only metaclass still checks `__init_subclass__()` | no | yes | yes | no | +| `__prepare__()` implied call (may) | no | no | no | no | +| Direct-call `__init_subclass__()` forwarding (may) | no | no | no | no | +| `**kwargs` metaclass forwarding (may) | no | no | yes | no | From 216020a4ef309a7b38caa19eec703d053daedb68 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:22:41 +0200 Subject: [PATCH 3/5] Always respect return type for class statements --- docs/spec/constructors.rst | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/spec/constructors.rst b/docs/spec/constructors.rst index c5aa10b1e..29d02a5bd 100644 --- a/docs/spec/constructors.rst +++ b/docs/spec/constructors.rst @@ -667,11 +667,17 @@ Keyword arguments in a direct metaclass call (such as the last two calls in the example above) require no special handling: they are validated as part of evaluating the call using the standard constructor call rules. -Regardless of the evaluated return type of the implied metaclass call, a -:keyword:`class` statement defines a class, and type checkers should evaluate the -type of the bound name accordingly (``type[MyClass1]`` in the example above). -The implied metaclass call is evaluated only for the purpose of validating -its arguments. +Type checkers should honor the evaluated retunr type of the implied metaclass call, +even if the evaluated type isn't a class:: + + class Meta(type): + def __new__(cls, *args: object, **kwargs: obect) -> int: + return 1 + + class MyClass6(metaclass=Meta): + pass + + assert_type(MyClass6, int) Type checkers may validate the implied call to :attr:`!__prepare__`: From 7fff12043fe114689a15e6fbea12ba102669a4f4 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:43:26 +0200 Subject: [PATCH 4/5] Typos, nits --- docs/spec/constructors.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/spec/constructors.rst b/docs/spec/constructors.rst index 29d02a5bd..51c6d2f50 100644 --- a/docs/spec/constructors.rst +++ b/docs/spec/constructors.rst @@ -667,11 +667,13 @@ Keyword arguments in a direct metaclass call (such as the last two calls in the example above) require no special handling: they are validated as part of evaluating the call using the standard constructor call rules. -Type checkers should honor the evaluated retunr type of the implied metaclass call, -even if the evaluated type isn't a class:: +Type checkers should honor the evaluated return type of the implied metaclass call, +even if the evaluated type isn't a class: + + :: class Meta(type): - def __new__(cls, *args: object, **kwargs: obect) -> int: + def __new__(mcls, *args: object, **kwargs: object) -> int: return 1 class MyClass6(metaclass=Meta): @@ -699,7 +701,7 @@ Type checkers may validate the implied call to :attr:`!__prepare__`: return super().__new__(mcls, name, bases, namespace) # The 'key' argument may result in a type checker error: - class MyClass6(metaclass=Meta, key=3): + class MyClass7(metaclass=Meta, key=3): pass The ``metaclass`` argument can also be an arbitrary callable that is not a subclass From f53025344db9d59f78e6b12b7f0db7348c18be18 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:21:53 +0200 Subject: [PATCH 5/5] Add conformance tests --- .../mypy/constructors_class_statements.toml | 27 ++ .../mypy/constructors_init_subclass.toml | 13 + .../results/mypy/constructors_metaclass.toml | 19 + .../mypy/constructors_type_constructor.toml | 15 + .../constructors_class_statements.toml | 16 + .../constructors_init_subclass.toml | 17 + .../pycroscope/constructors_metaclass.toml | 8 + .../constructors_type_constructor.toml | 15 + .../constructors_class_statements.toml | 17 + .../pyrefly/constructors_init_subclass.toml | 14 + .../pyrefly/constructors_metaclass.toml | 8 + .../constructors_type_constructor.toml | 12 + .../constructors_class_statements.toml | 20 + .../pyright/constructors_init_subclass.toml | 15 + .../pyright/constructors_metaclass.toml | 9 + .../constructors_type_constructor.toml | 16 + conformance/results/results.html | 161 +++++++- .../ty/constructors_class_statements.toml | 17 + .../ty/constructors_init_subclass.toml | 14 + .../results/ty/constructors_metaclass.toml | 8 + .../ty/constructors_type_constructor.toml | 12 + .../zuban/constructors_class_statements.toml | 23 ++ .../zuban/constructors_init_subclass.toml | 13 + .../results/zuban/constructors_metaclass.toml | 8 + .../zuban/constructors_type_constructor.toml | 18 + .../tests/constructors_class_statements.py | 110 ++++++ .../tests/constructors_init_subclass.py | 95 +++++ conformance/tests/constructors_metaclass.py | 67 ++++ .../tests/constructors_type_constructor.py | 52 +++ metaclass-constructors-checkers.md | 356 ------------------ 30 files changed, 827 insertions(+), 368 deletions(-) create mode 100644 conformance/results/mypy/constructors_class_statements.toml create mode 100644 conformance/results/mypy/constructors_init_subclass.toml create mode 100644 conformance/results/mypy/constructors_metaclass.toml create mode 100644 conformance/results/mypy/constructors_type_constructor.toml create mode 100644 conformance/results/pycroscope/constructors_class_statements.toml create mode 100644 conformance/results/pycroscope/constructors_init_subclass.toml create mode 100644 conformance/results/pycroscope/constructors_metaclass.toml create mode 100644 conformance/results/pycroscope/constructors_type_constructor.toml create mode 100644 conformance/results/pyrefly/constructors_class_statements.toml create mode 100644 conformance/results/pyrefly/constructors_init_subclass.toml create mode 100644 conformance/results/pyrefly/constructors_metaclass.toml create mode 100644 conformance/results/pyrefly/constructors_type_constructor.toml create mode 100644 conformance/results/pyright/constructors_class_statements.toml create mode 100644 conformance/results/pyright/constructors_init_subclass.toml create mode 100644 conformance/results/pyright/constructors_metaclass.toml create mode 100644 conformance/results/pyright/constructors_type_constructor.toml create mode 100644 conformance/results/ty/constructors_class_statements.toml create mode 100644 conformance/results/ty/constructors_init_subclass.toml create mode 100644 conformance/results/ty/constructors_metaclass.toml create mode 100644 conformance/results/ty/constructors_type_constructor.toml create mode 100644 conformance/results/zuban/constructors_class_statements.toml create mode 100644 conformance/results/zuban/constructors_init_subclass.toml create mode 100644 conformance/results/zuban/constructors_metaclass.toml create mode 100644 conformance/results/zuban/constructors_type_constructor.toml create mode 100644 conformance/tests/constructors_class_statements.py create mode 100644 conformance/tests/constructors_init_subclass.py create mode 100644 conformance/tests/constructors_metaclass.py create mode 100644 conformance/tests/constructors_type_constructor.py delete mode 100644 metaclass-constructors-checkers.md diff --git a/conformance/results/mypy/constructors_class_statements.toml b/conformance/results/mypy/constructors_class_statements.toml new file mode 100644 index 000000000..ec36fe665 --- /dev/null +++ b/conformance/results/mypy/constructors_class_statements.toml @@ -0,0 +1,27 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +Incorrectly rejects a metaclass `__new__` method annotated to return a type other than a class. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 77: Unexpected errors ['constructors_class_statements.py:77: error: Incompatible return type for "__new__" (returns "int", but must return a subtype of "type") [misc]'] +Line 85: Unexpected errors ['constructors_class_statements.py:85: error: Expression is of type "type[Class6]", not "int" [assert-type]'] +""" +output = """ +constructors_class_statements.py:30: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [metaclass] +constructors_class_statements.py:30: note: "constructors_class_statements.MetaA" (metaclass of "constructors_class_statements.BaseA") conflicts with "constructors_class_statements.MetaB" (metaclass of "constructors_class_statements.BaseB") +constructors_class_statements.py:69: error: Argument "key" to "Meta1" has incompatible type "str"; expected "int" [arg-type] +constructors_class_statements.py:77: error: Incompatible return type for "__new__" (returns "int", but must return a subtype of "type") [misc] +constructors_class_statements.py:85: error: Expression is of type "type[Class6]", not "int" [assert-type] +constructors_class_statements.py:93: error: Signature of "__prepare__" incompatible with supertype "builtins.type" [override] +constructors_class_statements.py:93: note: Superclass: +constructors_class_statements.py:93: note: @classmethod +constructors_class_statements.py:93: note: def __prepare__(metacls, str, tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] +constructors_class_statements.py:93: note: Subclass: +constructors_class_statements.py:93: note: @classmethod +constructors_class_statements.py:93: note: def __prepare__(mcls, name: str, bases: tuple[type, ...]) -> Any +""" diff --git a/conformance/results/mypy/constructors_init_subclass.toml b/conformance/results/mypy/constructors_init_subclass.toml new file mode 100644 index 000000000..d0ee1c849 --- /dev/null +++ b/conformance/results/mypy/constructors_init_subclass.toml @@ -0,0 +1,13 @@ +conformant = "Partial" +notes = """ +Does not validate keyword arguments against `__init_subclass__` when the metaclass defines an `__init__` method (but no `__new__` method). +""" +conformance_automated = "Fail" +errors_diff = """ +Line 58: Expected 1 errors +""" +output = """ +constructors_init_subclass.py:26: error: Argument "flag" to "__init_subclass__" of "Base" has incompatible type "str"; expected "bool" [arg-type] +constructors_init_subclass.py:30: error: Unexpected keyword argument "other" for "__init_subclass__" of "Base" [call-arg] +constructors_init_subclass.py:34: error: Unexpected keyword argument "other" for "__init_subclass__" of "object" [call-arg] +""" diff --git a/conformance/results/mypy/constructors_metaclass.toml b/conformance/results/mypy/constructors_metaclass.toml new file mode 100644 index 000000000..892fb934c --- /dev/null +++ b/conformance/results/mypy/constructors_metaclass.toml @@ -0,0 +1,19 @@ +conformant = "Partial" +notes = """ +Does not evaluate the `__call__` method of the metaclass's own metaclass. +Incorrectly rejects a metaclass `__new__` method annotated to return a type other than a class. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 28: Unexpected errors ['constructors_metaclass.py:28: error: Expression is of type "Meta1", not "Never" [assert-type]'] +Line 57: Unexpected errors ['constructors_metaclass.py:57: error: Incompatible return type for "__new__" (returns "int", but must return a subtype of "type") [misc]'] +Line 67: Unexpected errors ['constructors_metaclass.py:67: error: Expression is of type "Meta3", not "int" [assert-type]', 'constructors_metaclass.py:67: error: Too many arguments for "Meta3" [call-arg]'] +""" +output = """ +constructors_metaclass.py:28: error: Expression is of type "Meta1", not "Never" [assert-type] +constructors_metaclass.py:47: error: Missing named argument "key" for "Meta2" [call-arg] +constructors_metaclass.py:48: error: Argument "key" to "Meta2" has incompatible type "str"; expected "int" [arg-type] +constructors_metaclass.py:57: error: Incompatible return type for "__new__" (returns "int", but must return a subtype of "type") [misc] +constructors_metaclass.py:67: error: Expression is of type "Meta3", not "int" [assert-type] +constructors_metaclass.py:67: error: Too many arguments for "Meta3" [call-arg] +""" diff --git a/conformance/results/mypy/constructors_type_constructor.toml b/conformance/results/mypy/constructors_type_constructor.toml new file mode 100644 index 000000000..5184d8fde --- /dev/null +++ b/conformance/results/mypy/constructors_type_constructor.toml @@ -0,0 +1,15 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +""" +output = """ +constructors_type_constructor.py:30: error: No overload variant of "type" matches argument types "str", "tuple[()]" [call-overload] +constructors_type_constructor.py:30: note: Possible overload variants: +constructors_type_constructor.py:30: note: def type(object, /) -> type +constructors_type_constructor.py:30: note: def type(str, tuple[type, ...], dict[str, Any], /, **kwds: Any) -> type +constructors_type_constructor.py:52: error: Argument 1 to "MetaSingle" has incompatible type "str"; expected "int" [arg-type] +""" diff --git a/conformance/results/pycroscope/constructors_class_statements.toml b/conformance/results/pycroscope/constructors_class_statements.toml new file mode 100644 index 000000000..9677f30a7 --- /dev/null +++ b/conformance/results/pycroscope/constructors_class_statements.toml @@ -0,0 +1,16 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 85: Unexpected errors ["./constructors_class_statements.py:85:12: is not equivalent to int"] +""" +output = """ +./constructors_class_statements.py:69:28: Incompatible argument type for key: expected int but got Literal[''] [incompatible_argument] +./constructors_class_statements.py:85:12: is not equivalent to int +./constructors_class_statements.py:93:4: Value of __prepare__ incompatible with base class type [incompatible_override] +""" diff --git a/conformance/results/pycroscope/constructors_init_subclass.toml b/conformance/results/pycroscope/constructors_init_subclass.toml new file mode 100644 index 000000000..134767bc8 --- /dev/null +++ b/conformance/results/pycroscope/constructors_init_subclass.toml @@ -0,0 +1,17 @@ +conformant = "Partial" +notes = """ +Does not validate class statement keyword arguments against `__init_subclass__`. +Incorrectly rejects a call to `type` with three positional arguments and additional keyword arguments. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 26: Expected 1 errors +Line 30: Expected 1 errors +Line 34: Expected 1 errors +Line 58: Expected 1 errors +Line 67: Unexpected errors ['./constructors_init_subclass.py:67:0: Cannot call overloaded function [incompatible_call]'] +""" +output = """ +./constructors_init_subclass.py:67:0: Cannot call overloaded function [incompatible_call] +./constructors_init_subclass.py:68:0: Cannot call overloaded function [incompatible_call] +""" diff --git a/conformance/results/pycroscope/constructors_metaclass.toml b/conformance/results/pycroscope/constructors_metaclass.toml new file mode 100644 index 000000000..e6432f2fd --- /dev/null +++ b/conformance/results/pycroscope/constructors_metaclass.toml @@ -0,0 +1,8 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +./constructors_metaclass.py:47:0: Missing required argument 'key' [incompatible_call] +./constructors_metaclass.py:48:23: Incompatible argument type for key: expected int but got Literal[''] [incompatible_argument] +""" diff --git a/conformance/results/pycroscope/constructors_type_constructor.toml b/conformance/results/pycroscope/constructors_type_constructor.toml new file mode 100644 index 000000000..e966dea43 --- /dev/null +++ b/conformance/results/pycroscope/constructors_type_constructor.toml @@ -0,0 +1,15 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +Does not evaluate the result of a single-argument call to `type` as `type[T]`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +Line 28: Unexpected errors ["./constructors_type_constructor.py:28:12: type 'int' is not equivalent to type[int]"] +""" +output = """ +./constructors_type_constructor.py:28:12: type 'int' is not equivalent to type[int] +./constructors_type_constructor.py:30:0: Cannot call overloaded function [incompatible_call] +./constructors_type_constructor.py:52:11: Incompatible argument type for x: expected int but got Literal[''] [incompatible_argument] +""" diff --git a/conformance/results/pyrefly/constructors_class_statements.toml b/conformance/results/pyrefly/constructors_class_statements.toml new file mode 100644 index 000000000..d41c5fa79 --- /dev/null +++ b/conformance/results/pyrefly/constructors_class_statements.toml @@ -0,0 +1,17 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 85: Unexpected errors ['assert_type(type[Class6], int) failed [assert-type]'] +""" +output = """ +ERROR constructors_class_statements.py:30:7-20: Class `ClassConflict` has metaclass `MetaA` from base class `BaseA` which is not compatible with metaclass `MetaB` from base class `BaseB` [invalid-inheritance] +ERROR constructors_class_statements.py:69:29-31: Argument `Literal['']` is not assignable to parameter `key` with type `int` in function `Meta1.__new__` [bad-argument-type] +ERROR constructors_class_statements.py:85:12-25: assert_type(type[Class6], int) failed [assert-type] +ERROR constructors_class_statements.py:93:9-20: Class member `Meta3.__prepare__` overrides parent class `type` in an inconsistent manner [bad-override] +""" diff --git a/conformance/results/pyrefly/constructors_init_subclass.toml b/conformance/results/pyrefly/constructors_init_subclass.toml new file mode 100644 index 000000000..f9b1e8618 --- /dev/null +++ b/conformance/results/pyrefly/constructors_init_subclass.toml @@ -0,0 +1,14 @@ +conformant = "Partial" +notes = """ +Does not validate keyword arguments against `object.__init_subclass__` in a class statement with no explicit base classes. +Does not validate keyword arguments against `__init_subclass__` when the metaclass defines an `__init__` method (but no `__new__` method). +""" +conformance_automated = "Fail" +errors_diff = """ +Line 34: Expected 1 errors +Line 58: Expected 1 errors +""" +output = """ +ERROR constructors_init_subclass.py:26:25-27: Argument `Literal['']` is not assignable to parameter `flag` with type `bool` in function `Base.__init_subclass__` [bad-argument-type] +ERROR constructors_init_subclass.py:30:20-25: Unexpected keyword argument `other` in function `Base.__init_subclass__` [unexpected-keyword] +""" diff --git a/conformance/results/pyrefly/constructors_metaclass.toml b/conformance/results/pyrefly/constructors_metaclass.toml new file mode 100644 index 000000000..d9574d97b --- /dev/null +++ b/conformance/results/pyrefly/constructors_metaclass.toml @@ -0,0 +1,8 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +ERROR constructors_metaclass.py:47:6-19: Missing argument `key` in function `Meta2.__new__` [missing-argument] +ERROR constructors_metaclass.py:48:24-26: Argument `Literal['']` is not assignable to parameter `key` with type `int` in function `Meta2.__new__` [bad-argument-type] +""" diff --git a/conformance/results/pyrefly/constructors_type_constructor.toml b/conformance/results/pyrefly/constructors_type_constructor.toml new file mode 100644 index 000000000..3243d0fa9 --- /dev/null +++ b/conformance/results/pyrefly/constructors_type_constructor.toml @@ -0,0 +1,12 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +""" +output = """ +ERROR constructors_type_constructor.py:30:5-14: No matching overload found for function `type.__new__` called with arguments: (type[type], Literal['A'], tuple[()]) [no-matching-overload] +ERROR constructors_type_constructor.py:52:12-14: Argument `Literal['']` is not assignable to parameter `x` with type `int` in function `MetaSingle.__new__` [bad-argument-type] +""" diff --git a/conformance/results/pyright/constructors_class_statements.toml b/conformance/results/pyright/constructors_class_statements.toml new file mode 100644 index 000000000..3999a182f --- /dev/null +++ b/conformance/results/pyright/constructors_class_statements.toml @@ -0,0 +1,20 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 85: Unexpected errors ['constructors_class_statements.py:85:13 - error: "assert_type" mismatch: expected "int" but received "type[Class6]" (reportAssertTypeFailure)'] +""" +output = """ +constructors_class_statements.py:30:7 - error: The metaclass of a derived class must be a subclass of the metaclasses of all its base classes +  Metaclass "MetaA" conflicts with "MetaB" (reportGeneralTypeIssues) +constructors_class_statements.py:56:35 - error: Argument of type "Literal['']" cannot be assigned to parameter of type "int" in function "__new__" +  "Literal['']" is not assignable to "int" (reportArgumentType) +constructors_class_statements.py:60:7 - error: Argument missing for parameter "key" (reportGeneralTypeIssues) +constructors_class_statements.py:69:29 - error: Argument of type "Literal['']" cannot be assigned to parameter "key" of type "int" in function "__new__" +  "Literal['']" is not assignable to "int" (reportArgumentType) +constructors_class_statements.py:85:13 - error: "assert_type" mismatch: expected "int" but received "type[Class6]" (reportAssertTypeFailure) +constructors_class_statements.py:93:9 - error: Method "__prepare__" overrides class "type" in an incompatible manner +  Parameter "**kwds" has no corresponding parameter (reportIncompatibleMethodOverride) +""" diff --git a/conformance/results/pyright/constructors_init_subclass.toml b/conformance/results/pyright/constructors_init_subclass.toml new file mode 100644 index 000000000..060e4a024 --- /dev/null +++ b/conformance/results/pyright/constructors_init_subclass.toml @@ -0,0 +1,15 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +constructors_init_subclass.py:26:7 - error: Incorrect keyword arguments for __init_subclass__ method (reportGeneralTypeIssues) +constructors_init_subclass.py:26:25 - error: Argument of type "Literal['']" cannot be assigned to parameter "flag" of type "bool" in function "__init_subclass__" +  "Literal['']" is not assignable to "bool" (reportArgumentType) +constructors_init_subclass.py:30:7 - error: Incorrect keyword arguments for __init_subclass__ method (reportGeneralTypeIssues) +constructors_init_subclass.py:30:20 - error: No parameter named "other" (reportCallIssue) +constructors_init_subclass.py:34:7 - error: Incorrect keyword arguments for __init_subclass__ method (reportGeneralTypeIssues) +constructors_init_subclass.py:34:14 - error: No parameter named "other" (reportCallIssue) +constructors_init_subclass.py:58:7 - error: Incorrect keyword arguments for __init_subclass__ method (reportGeneralTypeIssues) +constructors_init_subclass.py:58:40 - error: No parameter named "key" (reportCallIssue) +""" diff --git a/conformance/results/pyright/constructors_metaclass.toml b/conformance/results/pyright/constructors_metaclass.toml new file mode 100644 index 000000000..93855ac70 --- /dev/null +++ b/conformance/results/pyright/constructors_metaclass.toml @@ -0,0 +1,9 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +constructors_metaclass.py:47:1 - error: Argument missing for parameter "key" (reportCallIssue) +constructors_metaclass.py:48:24 - error: Argument of type "Literal['']" cannot be assigned to parameter "key" of type "int" in function "__new__" +  "Literal['']" is not assignable to "int" (reportArgumentType) +""" diff --git a/conformance/results/pyright/constructors_type_constructor.toml b/conformance/results/pyright/constructors_type_constructor.toml new file mode 100644 index 000000000..d4f86d945 --- /dev/null +++ b/conformance/results/pyright/constructors_type_constructor.toml @@ -0,0 +1,16 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +""" +output = """ +constructors_type_constructor.py:30:1 - error: No overloads for "__new__" match the provided arguments +  Argument types: (Literal['A'], tuple[()]) (reportCallIssue) +constructors_type_constructor.py:30:1 - error: No overloads for "__init__" match the provided arguments +  Argument types: (Literal['A'], tuple[()]) (reportCallIssue) +constructors_type_constructor.py:52:12 - error: Argument of type "Literal['']" cannot be assigned to parameter "x" of type "int" in function "__new__" +  "Literal['']" is not assignable to "int" (reportArgumentType) +""" diff --git a/conformance/results/results.html b/conformance/results/results.html index 3877f3c17..1e445a5bc 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -1615,6 +1615,51 @@

Python Type System Conformance Test Results

Pass + + constructors_class_statements + + Partial +
    +
  • Does not honor the evaluated return type of the implied metaclass call when it isn't a class.
  • +
  • Does not validate keyword arguments in a class statement against the metaclass __new__ method.
  • +
  • Incorrectly rejects a metaclass __new__ method annotated to return a type other than a class.
  • +
+ + + Partial +
    +
  • Does not honor the evaluated return type of the implied metaclass call when it isn't a class.
  • +
  • Does not validate keyword arguments in a class statement against the metaclass __new__ method.
  • +
+ + + Partial +
    +
  • Does not honor the evaluated return type of the implied metaclass call when it isn't a class.
  • +
  • Does not validate keyword arguments in a class statement against the metaclass __new__ method.
  • +
+ + + Partial +
    +
  • Does not honor the evaluated return type of the implied metaclass call when it isn't a class.
  • +
+ + + Partial +
    +
  • Does not honor the evaluated return type of the implied metaclass call when it isn't a class.
  • +
  • Does not validate keyword arguments in a class statement against the metaclass __new__ method.
  • +
+ + + Partial +
    +
  • Does not honor the evaluated return type of the implied metaclass call when it isn't a class.
  • +
  • Does not validate keyword arguments in a class statement against the metaclass __new__ method.
  • +
+ + constructors_consistency @@ -1629,14 +1674,106 @@

Python Type System Conformance Test Results

Pass Pass + + constructors_init_subclass + + Partial +
    +
  • Does not validate keyword arguments against __init_subclass__ when the metaclass defines an __init__ method (but no __new__ method).
  • +
+ + + Partial +
    +
  • Does not validate class statement keyword arguments against __init_subclass__.
  • +
  • Incorrectly rejects a call to type with three positional arguments and additional keyword arguments.
  • +
+ + + Partial +
    +
  • Does not validate keyword arguments against object.__init_subclass__ in a class statement with no explicit base classes.
  • +
  • Does not validate keyword arguments against __init_subclass__ when the metaclass defines an __init__ method (but no __new__ method).
  • +
+ + Pass + + Partial +
    +
  • Does not validate keyword arguments against object.__init_subclass__ in a class statement with no explicit base classes.
  • +
+ + + Partial +
    +
  • Does not validate keyword arguments against __init_subclass__ when the metaclass defines an __init__ method (but no __new__ method).
  • +
+ + + + constructors_metaclass + + Partial +
    +
  • Does not evaluate the __call__ method of the metaclass's own metaclass.
  • +
  • Incorrectly rejects a metaclass __new__ method annotated to return a type other than a class.
  • +
+ + Pass + Pass + Pass + Pass + Pass + + + constructors_type_constructor + + Partial +
    +
  • Does not report a single-argument call to a subclass of type.
  • +
+ + + Partial +
    +
  • Does not report a single-argument call to a subclass of type.
  • +
  • Does not evaluate the result of a single-argument call to type as type[T].
  • +
+ + + Partial +
    +
  • Does not report a single-argument call to a subclass of type.
  • +
+ + + Partial +
    +
  • Does not report a single-argument call to a subclass of type.
  • +
+ + + Partial +
    +
  • Does not report a single-argument call to a subclass of type.
  • +
+ + + Partial +
    +
  • Does not report a single-argument call to a subclass of type.
  • +
  • Does not distribute the evaluated type of a single-argument call to type over union types.
  • +
+ + - 3 / 6 • 50.0% - 6 / 6 • 100.0% - 6 / 6 • 100.0% - 6 / 6 • 100.0% - 5 / 6 • 83.3% - 6 / 6 • 100.0% + 5 / 10 • 50.0% + 8.5 / 10 • 85.0% + 8.5 / 10 • 85.0% + 9 / 10 • 90.0% + 7.5 / 10 • 75.0% + 8.5 / 10 • 85.0% @@ -2583,12 +2720,12 @@

Python Type System Conformance Test Results

- 109 / 146 • 74.7% - 138.5 / 146 • 94.9% - 141 / 146 • 96.6% - 135.5 / 146 • 92.8% - 139.5 / 146 • 95.5% - 146 / 146 • 100.0% + 111 / 150 • 74.0% + 141 / 150 • 94.0% + 143.5 / 150 • 95.7% + 138.5 / 150 • 92.3% + 142 / 150 • 94.7% + 148.5 / 150 • 99.0% diff --git a/conformance/results/ty/constructors_class_statements.toml b/conformance/results/ty/constructors_class_statements.toml new file mode 100644 index 000000000..cf8dcc9da --- /dev/null +++ b/conformance/results/ty/constructors_class_statements.toml @@ -0,0 +1,17 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 85: Unexpected errors ["constructors_class_statements.py:85:1: error[type-assertion-failure] Type `` does not match asserted type `int`"] +""" +output = """ +constructors_class_statements.py:30:1: error[conflicting-metaclass] The metaclass of a derived class (`ClassConflict`) must be a subclass of the metaclasses of all its bases, but `MetaA` (metaclass of base class `BaseA`) and `MetaB` (metaclass of base class `BaseB`) have no subclass relationship +constructors_class_statements.py:69:25: error[invalid-argument-type] Argument to constructor `Meta1.__new__` is incorrect: Expected `int`, found `Literal[""]` +constructors_class_statements.py:85:1: error[type-assertion-failure] Type `` does not match asserted type `int` +constructors_class_statements.py:93:9: error[invalid-method-override] Invalid override of method `__prepare__`: Definition is incompatible with `type.__prepare__` +""" diff --git a/conformance/results/ty/constructors_init_subclass.toml b/conformance/results/ty/constructors_init_subclass.toml new file mode 100644 index 000000000..d59949e2b --- /dev/null +++ b/conformance/results/ty/constructors_init_subclass.toml @@ -0,0 +1,14 @@ +conformant = "Partial" +notes = """ +Does not validate keyword arguments against `object.__init_subclass__` in a class statement with no explicit base classes. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 34: Expected 1 errors +""" +output = """ +constructors_init_subclass.py:26:20: error[invalid-argument-type] Argument to function `Base.__init_subclass__` is incorrect: Expected `bool`, found `Literal[""]` +constructors_init_subclass.py:30:20: error[unknown-argument] Argument `other` does not match any known parameter of function `Base.__init_subclass__` +constructors_init_subclass.py:58:40: error[unknown-argument] Argument `key` does not match any known parameter of function `Base.__init_subclass__` +constructors_init_subclass.py:94:42: error[unknown-argument] Argument `other` does not match any known parameter of function `Base.__init_subclass__` +""" diff --git a/conformance/results/ty/constructors_metaclass.toml b/conformance/results/ty/constructors_metaclass.toml new file mode 100644 index 000000000..4926810b9 --- /dev/null +++ b/conformance/results/ty/constructors_metaclass.toml @@ -0,0 +1,8 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +constructors_metaclass.py:47:1: error[missing-argument] No argument provided for required parameter `key` of constructor `Meta2.__new__` +constructors_metaclass.py:48:20: error[invalid-argument-type] Argument to constructor `Meta2.__new__` is incorrect: Expected `int`, found `Literal[""]` +""" diff --git a/conformance/results/ty/constructors_type_constructor.toml b/conformance/results/ty/constructors_type_constructor.toml new file mode 100644 index 000000000..271361490 --- /dev/null +++ b/conformance/results/ty/constructors_type_constructor.toml @@ -0,0 +1,12 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +""" +output = """ +constructors_type_constructor.py:30:1: error[no-matching-overload] No overload of class `type` matches arguments +constructors_type_constructor.py:52:12: error[invalid-argument-type] Argument to constructor `MetaSingle.__new__` is incorrect: Expected `int`, found `Literal[""]` +""" diff --git a/conformance/results/zuban/constructors_class_statements.toml b/conformance/results/zuban/constructors_class_statements.toml new file mode 100644 index 000000000..36cc58b3e --- /dev/null +++ b/conformance/results/zuban/constructors_class_statements.toml @@ -0,0 +1,23 @@ +conformant = "Partial" +notes = """ +Does not honor the evaluated return type of the implied metaclass call when it isn't a class. +Does not validate keyword arguments in a class statement against the metaclass `__new__` method. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 56: Expected 1 errors +Line 60: Expected 1 errors +Line 85: Unexpected errors ['constructors_class_statements.py:85: error: Expression is of type "type[Class6]", not "int" [misc]'] +""" +output = """ +constructors_class_statements.py:30: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [metaclass] +constructors_class_statements.py:69: error: Argument "key" to "Meta1" has incompatible type "str"; expected "int" [arg-type] +constructors_class_statements.py:85: error: Expression is of type "type[Class6]", not "int" [misc] +constructors_class_statements.py:93: error: Signature of "__prepare__" incompatible with supertype "builtins.type" [override] +constructors_class_statements.py:93: note: Superclass: +constructors_class_statements.py:93: note: @classmethod +constructors_class_statements.py:93: note: def __prepare__(cls, str, tuple[type[Any], ...], /, **kwds: Any) -> MutableMapping[str, object] +constructors_class_statements.py:93: note: Subclass: +constructors_class_statements.py:93: note: @classmethod +constructors_class_statements.py:93: note: def __prepare__(cls, name: str, bases: tuple[type[Any], ...]) -> dict[Any, Any] +""" diff --git a/conformance/results/zuban/constructors_init_subclass.toml b/conformance/results/zuban/constructors_init_subclass.toml new file mode 100644 index 000000000..d0ee1c849 --- /dev/null +++ b/conformance/results/zuban/constructors_init_subclass.toml @@ -0,0 +1,13 @@ +conformant = "Partial" +notes = """ +Does not validate keyword arguments against `__init_subclass__` when the metaclass defines an `__init__` method (but no `__new__` method). +""" +conformance_automated = "Fail" +errors_diff = """ +Line 58: Expected 1 errors +""" +output = """ +constructors_init_subclass.py:26: error: Argument "flag" to "__init_subclass__" of "Base" has incompatible type "str"; expected "bool" [arg-type] +constructors_init_subclass.py:30: error: Unexpected keyword argument "other" for "__init_subclass__" of "Base" [call-arg] +constructors_init_subclass.py:34: error: Unexpected keyword argument "other" for "__init_subclass__" of "object" [call-arg] +""" diff --git a/conformance/results/zuban/constructors_metaclass.toml b/conformance/results/zuban/constructors_metaclass.toml new file mode 100644 index 000000000..83ca58b5e --- /dev/null +++ b/conformance/results/zuban/constructors_metaclass.toml @@ -0,0 +1,8 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +constructors_metaclass.py:47: error: Missing named argument "key" for "Meta2" [call-arg] +constructors_metaclass.py:48: error: Argument "key" to "Meta2" has incompatible type "str"; expected "int" [arg-type] +""" diff --git a/conformance/results/zuban/constructors_type_constructor.toml b/conformance/results/zuban/constructors_type_constructor.toml new file mode 100644 index 000000000..ffa0543f2 --- /dev/null +++ b/conformance/results/zuban/constructors_type_constructor.toml @@ -0,0 +1,18 @@ +conformant = "Partial" +notes = """ +Does not report a single-argument call to a subclass of `type`. +Does not distribute the evaluated type of a single-argument call to `type` over union types. +""" +conformance_automated = "Fail" +errors_diff = """ +Line 29: Expected 1 errors +Line 16: Unexpected errors ['constructors_type_constructor.py:16: error: Expression is of type "type[int | str]", not "type[int] | type[str]" [misc]'] +""" +output = """ +constructors_type_constructor.py:16: error: Expression is of type "type[int | str]", not "type[int] | type[str]" [misc] +constructors_type_constructor.py:30: error: No overload variant of "type" matches argument types "str", "tuple[()]" [call-overload] +constructors_type_constructor.py:30: note: Possible overload variants: +constructors_type_constructor.py:30: note: def __init__(self, object, /) -> type +constructors_type_constructor.py:30: note: def __init__(self, str, tuple[type[Any], ...], dict[str, Any], /, **kwds: Any) -> type +constructors_type_constructor.py:52: error: Argument 1 to "MetaSingle" has incompatible type "str"; expected "int" [arg-type] +""" diff --git a/conformance/tests/constructors_class_statements.py b/conformance/tests/constructors_class_statements.py new file mode 100644 index 000000000..6785b296d --- /dev/null +++ b/conformance/tests/constructors_class_statements.py @@ -0,0 +1,110 @@ +""" +Tests the evaluation of the implied metaclass call performed by class +statements. +""" + +from typing import Any, assert_type + +# Specification: https://typing.readthedocs.io/en/latest/spec/constructors.html#class-statements + +# > Type checkers may report an error for a class statement whose base classes +# > have incompatible metaclasses. + + +class MetaA(type): + pass + + +class MetaB(type): + pass + + +class BaseA(metaclass=MetaA): + pass + + +class BaseB(metaclass=MetaB): + pass + + +class ClassConflict(BaseA, BaseB): # E?: metaclass conflict + pass + + +# > Type checkers should validate keyword arguments in a class statement's +# > argument list (other than ``metaclass``) by evaluating the implied +# > metaclass call using the constructor call rules described in +# > :ref:`constructor-calls`. + + +class Meta1(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +class Class1(metaclass=Meta1, key=3): # OK + pass + + +class Class2(metaclass=Meta1, key=""): # E: wrong type for "key" + pass + + +class Class3(metaclass=Meta1): # E: missing keyword argument "key" + pass + + +# > Keyword arguments in a direct metaclass call (such as the last two calls +# > in the example above) require no special handling: they are validated as +# > part of evaluating the call using the standard constructor call rules. + +Meta1("Class4", (), {}, key=3) # OK +Meta1("Class5", (), {}, key="") # E: wrong type for "key" + + +# > Type checkers should honor the evaluated return type of the implied +# > metaclass call, even if the evaluated type isn't a class. + + +class Meta2(type): + def __new__(mcls, *args: object, **kwargs: object) -> int: + return 1 + + +class Class6(metaclass=Meta2): + pass + + +assert_type(Class6, int) + + +# > Type checkers may validate the implied call to __prepare__(). + + +class Meta3(type): + @classmethod + def __prepare__( # E?: incompatible override of type.__prepare__() (out of scope for this test) + mcls, name: str, bases: tuple[type, ...] + ): # No **kwds + return {} + + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +class Class7(metaclass=Meta3, key=3): # E?: "key" is not accepted by __prepare__() + pass diff --git a/conformance/tests/constructors_init_subclass.py b/conformance/tests/constructors_init_subclass.py new file mode 100644 index 000000000..8e00d1a3c --- /dev/null +++ b/conformance/tests/constructors_init_subclass.py @@ -0,0 +1,95 @@ +""" +Tests the validation of class statement keyword arguments against the +``__init_subclass__`` method of the parent class. +""" + +from typing import Any + +# Specification: https://typing.readthedocs.io/en/latest/spec/constructors.html#the-init-subclass-method + +# > If the metaclass of the class being defined does not define its own +# > __new__() method (including when no explicit metaclass is specified), +# > type checkers should validate the keyword arguments in a class statement's +# > argument list against the __init_subclass__() method of the parent +# > class. + + +class Base: + def __init_subclass__(cls, *, flag: bool = False) -> None: + super().__init_subclass__() + + +class Class1(Base, flag=True): # OK + pass + + +class Class2(Base, flag=""): # E: wrong type for "flag" + pass + + +class Class3(Base, other=1): # E: no parameter named "other" + pass + + +class Class4(other=1): # E: object.__init_subclass__() accepts no keyword arguments + pass + + +# > A metaclass __init__() method has no effect on this rule: when the +# > metaclass does not define its own __new__() method, type.__new__() +# > still forwards the keyword arguments to __init_subclass__(), so the +# > keyword arguments should satisfy both the metaclass __init__() method +# > (as part of validating the implied metaclass call) and the +# > __init_subclass__() method of the parent class. + + +class MetaInit(type): + def __init__( + cls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ) -> None: + super().__init__(name, bases, namespace) + + +class Class5(Base, metaclass=MetaInit, key=1): # E: "key" is not accepted by Base.__init_subclass__() + pass + + +# > The same forwarding occurs when the metaclass is called directly ... +# > Type checkers may validate keyword arguments in such calls against the +# > __init_subclass__() method of the parent class when the base classes can +# > be statically determined. + +type("ClassD", (Base,), {}, flag=True) # OK +type("ClassE", (Base,), {}, other=1) # E?: no parameter named "other" + + +# > If the metaclass defines its own __new__() method that accepts keyword +# > arguments only through a **kwargs parameter, whether these arguments +# > are forwarded to type.__new__() (and from there to +# > __init_subclass__()) cannot generally be determined statically. In this +# > situation, type checkers may additionally validate the keyword arguments +# > against the __init_subclass__() method of the parent class. + + +class MetaKwargs(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + **kwargs: Any, + ): + return super().__new__(mcls, name, bases, namespace, **kwargs) + + +class Class6(Base, metaclass=MetaKwargs, flag=True): # OK + pass + + +class Class7(Base, metaclass=MetaKwargs, other=1): # E?: no parameter named "other" + pass diff --git a/conformance/tests/constructors_metaclass.py b/conformance/tests/constructors_metaclass.py new file mode 100644 index 000000000..0e635e45e --- /dev/null +++ b/conformance/tests/constructors_metaclass.py @@ -0,0 +1,67 @@ +""" +Tests the evaluation of metaclass constructor calls (calls to a metaclass +that create a new class). +""" + +from typing import Any, Never, assert_type + +# Specification: https://typing.readthedocs.io/en/latest/spec/constructors.html#metaclass-constructors + +# > In both cases, the metaclass call should be evaluated using the same rules +# > described in the sections above: the __call__() method of the metaclass's +# > own metaclass (typically type.__call__()) is invoked, which in turn calls +# > the __new__() and __init__() methods of the metaclass. + + +class MetaMeta(type): + def __call__(cls, *args, **kwargs) -> Never: + raise TypeError("Classes cannot be created with this metaclass") + + +class Meta1(type, metaclass=MetaMeta): + pass + + +# This needs to be in a separate scope, because some type checkers might mark +# the statements after it as unreachable. +if bool(): + assert_type(Meta1("A", (), {}), Never) + + +class Meta2(type): + def __new__( + mcls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + *, + key: int, + ): + return super().__new__(mcls, name, bases, namespace) + + +# The return type is an instance of the metaclass being called, but type +# checkers may infer a more precise type, so assignability is checked +# instead of using assert_type(). +meta2_instance: Meta2 = Meta2("B", (), {}, key=1) # OK +Meta2("B", (), {}) # E: missing keyword argument "key" +Meta2("B", (), {}, key="") # E: wrong type for "key" + + +# > If the evaluated return type of __new__() is not the class being +# > constructed (or a subclass thereof), a type checker should assume that the +# > __init__() method will not be called. + + +class Meta3(type): + def __new__( + mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] + ) -> int: + return 0 + + # Not evaluated, as __new__() does not return an instance of Meta3: + def __init__(cls, x: str) -> None: + pass + + +assert_type(Meta3("C", (), {}), int) diff --git a/conformance/tests/constructors_type_constructor.py b/conformance/tests/constructors_type_constructor.py new file mode 100644 index 000000000..eb0bc8d45 --- /dev/null +++ b/conformance/tests/constructors_type_constructor.py @@ -0,0 +1,52 @@ +""" +Tests the special-case handling of calls to the ``type`` constructor. +""" + +from typing import assert_type + +# Specification: https://typing.readthedocs.io/en/latest/spec/constructors.html#the-type-constructor + +# > Although the single-argument form is typically declared with a return type +# > of `type`, type checkers should special-case this form and evaluate its +# > result as type[T], where T is the type of the argument. + + +def func1(x: int, y: int | str) -> None: + assert_type(type(x), type[int]) + assert_type(type(y), type[int] | type[str]) + + +# > At runtime, the single-argument form applies only when the class being +# > called is `type` itself, and is not inherited by metaclasses: a +# > single-argument call to a subclass of `type` raises a TypeError. + + +class Meta(type): + pass + + +assert_type(type(1), type[int]) # OK, uses the single-argument form +Meta(1) # E: single-argument form does not apply to subclasses +type("A", ()) # E: two-argument form does not exist + +# > The evaluated return type of the three-argument form is an instance of the +# > metaclass being called ... Type checkers may infer a more precise type for +# > the returned class object. + +meta_instance: Meta = Meta("A", (), {}) # OK, uses the three-argument form + + +# > This special-casing applies only to the __new__() and __init__() +# > methods inherited from `type`. If a metaclass defines its own +# > __new__() method that accepts a single argument, calls to it should be +# > evaluated using the rules for regular constructor calls described earlier +# > in this chapter. + + +class MetaSingle(type): + def __new__(mcls, x: int): + return super().__new__(mcls, "X", (), {}) + + +MetaSingle(1) # OK +MetaSingle("") # E: wrong type for "x" diff --git a/metaclass-constructors-checkers.md b/metaclass-constructors-checkers.md deleted file mode 100644 index c4c00c767..000000000 --- a/metaclass-constructors-checkers.md +++ /dev/null @@ -1,356 +0,0 @@ -# Metaclass constructors: spec examples vs. type checkers - -This document runs the examples from the [Metaclass Constructors](docs/spec/constructors.rst) -spec sections against CPython and four type checkers, to assess how far current -implementations are from the proposed behavior. - -Environment: - -- Runtime: CPython 3.14.5 -- mypy 2.3.0 (default settings) -- pyright 1.1.411 (default settings) -- ty 0.0.65 (default settings) -- pyrefly 1.1.1 (`--preset default`; the implicit `basic` preset disables - call-shape validation entirely and reports nothing on these examples) - -Each significant line is annotated with a comment of the form: - -``` -# spec: | runtime: | mypy: | pyright: | ty: | pyrefly: -``` - -- `spec:` is what the spec expects a type checker to report: `error` (should), - `may error` (optional), or `ok` (no error). For `assert_type()` lines, `ok` - means the assertion should pass. -- `runtime:` is what CPython does when the statement is executed. Note that a - line can be a type error while running fine (e.g. a wrongly typed keyword - value is not checked at runtime), and vice versa (an `assert_type()` whose - argument raises). -- A checker column says `error` if the checker reports any diagnostic on that - line, `ok` otherwise. - -## Metaclass Constructors (intro example) - -```python -from typing import Any, Never, assert_type - - -class MetaMeta(type): - def __call__(cls, *args, **kwargs) -> Never: - raise TypeError("Classes cannot be created with this metaclass") - - -class Meta1(type, metaclass=MetaMeta): - pass - - -# spec: ok | runtime: TypeError (by design) | mypy: error | pyright: ok | ty: ok | pyrefly: ok -assert_type(Meta1("A", (), {}), Never) - - -class Meta2(type): - def __new__( - mcls, - name: str, - bases: tuple[type, ...], - namespace: dict[str, Any], - *, - key: int, - ): - return super().__new__(mcls, name, bases, namespace) - - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -Meta2("B", (), {}, key=1) - -# spec: error | runtime: TypeError | mypy: error | pyright: error | ty: error | pyrefly: error -# runtime: TypeError: Meta2.__new__() missing 1 required keyword-only argument: 'key' -Meta2("B", (), {}) - - -class Meta3(type): - # spec: ok | mypy: error (rejects an int-returning __new__ at the definition) | pyright: ok | ty: ok | pyrefly: ok - def __new__( - mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] - ) -> int: - return 0 - - def __init__(cls, x: str) -> None: - pass - - -# spec: ok | runtime: ok | mypy: error | pyright: ok | ty: ok | pyrefly: ok -assert_type(Meta3("C", (), {}), int) -``` - -Notes: - -- pyright, ty, and pyrefly all honor the metametaclass `__call__()` returning - `Never` and the `__init__()`-skipping rule when `__new__()` returns `int` - (`reveal_type` confirms `Never` and `int` for ty and pyrefly). mypy evaluates - `Meta1(...)` as `Meta1` and rejects the `Meta3.__new__()` definition outright. -- The direct call missing `key` is flagged by **all four** checkers — direct - metaclass calls go through the standard constructor-call rules everywhere. - -## The `type` Constructor — single-argument form inference - -```python -from typing import assert_type - - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -def func(x: int, y: int | str) -> None: - assert_type(type(x), type[int]) - assert_type(type(y), type[int] | type[str]) -``` - -All four checkers already special-case `type(obj)` to `type[T]` -(pyrefly reveals exactly `type[int]`; ty infers the even more precise class -literal `` — see next example). - -## The `type` Constructor — single-argument form on subclasses - -```python -from typing import assert_type - - -class Meta(type): - pass - - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: error | pyrefly: ok -assert_type(type(1), type[int]) - -# spec: error | runtime: TypeError | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -# runtime: TypeError: type.__new__() takes exactly 3 arguments (1 given) -Meta(1) - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -Meta("A", (), {}) -``` - -Notes: - -- **No checker currently reports `Meta(1)`**: all four inherit the - single-argument overload of `type.__new__()`/`type.__init__()` into the - subclass. -- ty's error on `assert_type(type(1), type[int])` is an artifact of it inferring - the *more precise* class literal `` for `type(1)` and reporting - the mismatch as `assert-type-unspellable-subtype`; the inference itself is - compliant. - -## Class Statements — keyword arguments vs. the metaclass constructor - -```python -from typing import Any - - -class Meta(type): - def __new__( - mcls, - name: str, - bases: tuple[type, ...], - namespace: dict[str, Any], - *, - key: int, - ): - return super().__new__(mcls, name, bases, namespace) - - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -class MyClass1(metaclass=Meta, key=3): - pass - - -# spec: error (wrong type for "key") | runtime: ok | mypy: ok | pyright: error | ty: ok | pyrefly: ok -class MyClass2(metaclass=Meta, key=""): - pass - - -# spec: error (missing "key") | runtime: TypeError | mypy: ok | pyright: error | ty: ok | pyrefly: ok -# runtime: TypeError: Meta.__new__() missing 1 required keyword-only argument: 'key' -class MyClass3(metaclass=Meta): - pass - - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -Meta("MyClass4", (), {}, key=3) - -# spec: error (wrong type for "key") | runtime: ok | mypy: error | pyright: error | ty: error | pyrefly: error -Meta("MyClass5", (), {}, key="") -``` - -Notes: - -- Only pyright validates class-statement keyword arguments against a custom - metaclass `__new__()` today. mypy, ty, and pyrefly all miss `MyClass2` and - `MyClass3` — including the missing-argument case that fails at runtime. -- The equivalent *direct* calls are validated by all four checkers through the - standard constructor-call rules. - -## Class Statements — the implied `__prepare__` call - -```python -from typing import Any - - -class Meta(type): - # all four checkers report an override-incompatibility error at this - # definition (parameter "**kwds" missing vs. type.__prepare__); that check - # is unrelated to validating the implied __prepare__ call below - @classmethod - def __prepare__(mcls, name: str, bases: tuple[type, ...]): # No **kwds - return {} - - def __new__( - mcls, - name: str, - bases: tuple[type, ...], - namespace: dict[str, Any], - *, - key: int, - ): - return super().__new__(mcls, name, bases, namespace) - - -# spec: may error | runtime: TypeError | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -# runtime: TypeError: Meta.__prepare__() got an unexpected keyword argument 'key' -class MyClass6(metaclass=Meta, key=3): - pass -``` - -No checker validates the implied `__prepare__` call at the class statement -(consistent with the spec's "may"). All four do flag the `__prepare__` -*definition* as an incompatible override of `type.__prepare__()`, which -indirectly catches this class of bug. - -## The `__init_subclass__()` Method — no custom metaclass `__new__()` - -```python -class Base: - def __init_subclass__(cls, *, flag: bool = False) -> None: - super().__init_subclass__() - - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -class MyClass1(Base, flag=True): - pass - - -# spec: error (wrong type for "flag") | runtime: ok | mypy: error | pyright: error | ty: error | pyrefly: ok -class MyClass2(Base, flag=""): - pass - - -# spec: error (unknown argument) | runtime: TypeError | mypy: error | pyright: error | ty: error | pyrefly: ok -# runtime: TypeError: Base.__init_subclass__() got an unexpected keyword argument 'other' -class MyClass3(Base, other=1): - pass - - -# spec: error (object.__init_subclass__ accepts no kwargs) | runtime: TypeError | mypy: error | pyright: error | ty: ok | pyrefly: ok -# runtime: TypeError: MyClass4.__init_subclass__() takes no keyword arguments -class MyClass4(other=1): - pass -``` - -mypy, pyright, and ty implement this rule (ty misses only the -`object.__init_subclass__()` case); pyrefly performs no class-keyword validation. - -## The `__init_subclass__()` Method — metaclass with only a custom `__init__()` - -```python -from typing import Any - - -class Base: - def __init_subclass__(cls, *, flag: bool = False) -> None: - super().__init_subclass__() - - -class MetaInit(type): - def __init__( - cls, - name: str, - bases: tuple[type, ...], - namespace: dict[str, Any], - *, - key: int, - ) -> None: - super().__init__(name, bases, namespace) - - -# spec: error | runtime: TypeError | mypy: ok | pyright: error | ty: error | pyrefly: ok -# runtime: TypeError: Base.__init_subclass__() got an unexpected keyword argument 'key' -class MyClass5(Base, metaclass=MetaInit, key=1): - pass -``` - -`key` is accepted by `MetaInit.__init__()`, but `type.__new__()` (not -overridden) still forwards it to `Base.__init_subclass__()`, which rejects it. -pyright and ty report it; mypy and pyrefly do not. - -## The `__init_subclass__()` Method — forwarding through direct calls and `**kwargs` - -```python -from typing import Any - - -class Base: - def __init_subclass__(cls, *, flag: bool = False) -> None: - super().__init_subclass__() - - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -type("D", (Base,), {}, flag=True) - -# spec: may error | runtime: TypeError | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -# runtime: TypeError: Base.__init_subclass__() got an unexpected keyword argument 'other' -type("E", (Base,), {}, other=1) - - -class MetaKwargs(type): - def __new__( - mcls, - name: str, - bases: tuple[type, ...], - namespace: dict[str, Any], - **kwargs: Any, - ): - return super().__new__(mcls, name, bases, namespace, **kwargs) - - -# spec: ok | runtime: ok | mypy: ok | pyright: ok | ty: ok | pyrefly: ok -class MyClass7(Base, metaclass=MetaKwargs, flag=True): - pass - - -# spec: may error | runtime: TypeError | mypy: ok | pyright: ok | ty: error | pyrefly: ok -# runtime: TypeError: Base.__init_subclass__() got an unexpected keyword argument 'other' -class MyClass8(Base, metaclass=MetaKwargs, other=1): - pass -``` - -No checker validates `__init_subclass__()` through a *direct* `type(...)` call. -Only ty follows the forwarding through a `**kwargs`-accepting metaclass -`__new__()` in a class statement, a consequence of ty checking -`__init_subclass__()` unconditionally — which also produces false positives -when a strict metaclass *consumes* a keyword argument (e.g. -`class C(Base, metaclass=MetaStrict, key=1)` where `key` never reaches -`__init_subclass__()`). - -## Summary of divergences from the proposed spec text - -| Rule | mypy | pyright | ty | pyrefly | -| --- | --- | --- | --- | --- | -| `type(obj)` evaluates to `type[T]` | yes | yes | yes (more precise) | yes | -| One-argument form rejected on `type` subclasses | no | no | no | no | -| Metametaclass `__call__()` governs metaclass calls | no | yes | yes | yes | -| `__init__()` skipped when metaclass `__new__()` returns non-instance | no | yes | yes | yes | -| Direct metaclass call arguments validated | yes | yes | yes | yes | -| Class-statement kwargs vs. custom metaclass `__new__()` | no | yes | no | no | -| Class-statement kwargs vs. `__init_subclass__()` (should) | yes | yes | mostly | no | -| `__init__()`-only metaclass still checks `__init_subclass__()` | no | yes | yes | no | -| `__prepare__()` implied call (may) | no | no | no | no | -| Direct-call `__init_subclass__()` forwarding (may) | no | no | no | no | -| `**kwargs` metaclass forwarding (may) | no | no | yes | no |