crates/ty_python_semantic/resources/mdtest/metaclass.md
__call__ on metaclassWhen a metaclass defines a custom __call__ method, it controls what happens when the class is
called. If the metaclass __call__ returns an "instance type" (subtype of the class being
constructed), then the class' __new__ and __init__ are checked as usual (see
class/constructor.md). But if the metaclass __call__ returns a non-instance type, then __new__
and __init__ are skipped and the return type of __call__ is used directly.
__call__ returning non-instance typeclass Meta(type):
def __call__(cls, x: int, y: str) -> str:
return y
class Foo(metaclass=Meta): ...
reveal_type(Foo(1, "hello")) # revealed: str
a: str = Foo(1, "hello") # OK
__call__ takes precedence over __init__ and __new__class Meta(type):
def __call__(cls) -> str:
return "hello"
class Foo(metaclass=Meta):
def __new__(cls, x: int) -> "Foo":
return object.__new__(cls)
def __init__(self, x: int, y: int) -> None:
pass
# The metaclass __call__ takes precedence, so no arguments are needed
# and the return type is str, not Foo.
reveal_type(Foo()) # revealed: str
__call__ with wrong argumentsclass Meta(type):
def __call__(cls, x: int) -> int:
return x
class Foo(metaclass=Meta): ...
# error: [invalid-argument-type]
reveal_type(Foo("wrong")) # revealed: int
# error: [missing-argument]
reveal_type(Foo()) # revealed: int
# error: [too-many-positional-arguments]
reveal_type(Foo(1, 2)) # revealed: int
__call__ with TypeVar return typeWhen the metaclass __call__ returns a TypeVar bound to the class type, it's essentially a
pass-through to the normal constructor machinery. In this case, we should still check the __new__
and __init__ signatures.
from typing import TypeVar
T = TypeVar("T")
class Meta(type):
def __call__(cls: type[T], *args, **kwargs) -> T:
return object.__new__(cls)
class Foo(metaclass=Meta):
def __init__(self, x: int) -> None:
pass
# The metaclass __call__ returns T (bound to Foo), so we check __init__ parameters.
Foo() # error: [missing-argument]
reveal_type(Foo(1)) # revealed: Foo
__call__ with no return type annotationWhen the metaclass __call__ has no return type annotation (returns Unknown), we should still
check the __new__ and __init__ signatures, and infer the instance return type.
class Meta(type):
def __call__(cls, *args, **kwargs):
return object.__new__(cls)
class Foo(metaclass=Meta):
def __init__(self, x: int) -> None:
pass
# No return type annotation means we fall through to check __init__ parameters.
Foo() # error: [missing-argument]
reveal_type(Foo(1)) # revealed: Foo
__call__ with specific parametersWhen the metaclass __call__ has specific parameters (not just *args, **kwargs), we validate them
even when the return type is an instance type. Here both __new__ and __init__ accept anything,
so the errors must come from the metaclass __call__.
from typing import Any, TypeVar
T = TypeVar("T")
class Meta(type):
def __call__(cls: type[T], x: int) -> T:
return object.__new__(cls)
class Foo(metaclass=Meta):
def __new__(cls, *args: Any, **kwargs: Any) -> "Foo":
return object.__new__(cls)
def __init__(self, *args: Any, **kwargs: Any) -> None:
pass
# The metaclass `__call__` requires exactly one `int` argument.
# error: [invalid-argument-type]
reveal_type(Foo("wrong")) # revealed: Foo
# error: [missing-argument]
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments]
reveal_type(Foo(1, 2)) # revealed: Foo
reveal_type(Foo(1)) # revealed: Foo
__call__ returning the class instance typeWhen the metaclass __call__ returns the constructed class type (or a subclass), it's not
overriding normal construction. Per the spec, __new__/__init__ should still be evaluated.
class Meta(type):
def __call__(cls, *args, **kwargs) -> "Foo":
return super().__call__(*args, **kwargs)
class Foo(metaclass=Meta):
def __init__(self, x: int) -> None:
pass
# The metaclass __call__ returns Foo, so we fall through to check __init__.
Foo() # error: [missing-argument]
Foo("wrong") # error: [invalid-argument-type]
reveal_type(Foo(1)) # revealed: Foo
__call__ returning a specific class affects subclassesWhen a metaclass __call__ returns a specific class (e.g., -> Foo), this is an instance type for
Foo itself, so __init__ is checked. But for a subclass Bar(Foo), the return type Foo is NOT
an instance of Bar, so the metaclass __call__ is used directly and Bar.__init__ is skipped.
from typing import Any
class Meta(type):
def __call__(cls, *args: Any, **kwargs: Any) -> "Foo":
return super().__call__(*args, **kwargs)
class Foo(metaclass=Meta):
def __init__(self, x: int) -> None:
pass
class Bar(Foo):
def __init__(self, y: str) -> None:
pass
# For Foo: return type `Foo` IS an instance of `Foo`, so `__init__` is checked.
Foo() # error: [missing-argument]
reveal_type(Foo(1)) # revealed: Foo
# For Bar: return type `Foo` is NOT an instance of `Bar`, so `__init__` is
# skipped and the metaclass `__call__` (which accepts `*args, **kwargs`) is
# used directly.
reveal_type(Bar()) # revealed: Foo
reveal_type(Bar("hello")) # revealed: Foo
__call__ returning AnyWhen a metaclass __call__ returns Any, the spec says to assume that the return type is not an
instance of the class being constructed, so we use the metaclass __call__ signature directly and
skip __new__/__init__ validation. It's a bit odd to have different behavior for -> Any than
for no annotation, but that's what the spec says, and for now we follow it.
from typing import Any
class Meta(type):
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
return super().__call__(*args, **kwargs)
class Foo(metaclass=Meta):
def __init__(self, x: int) -> None:
pass
# The metaclass `__call__` accepts `(*args, **kwargs)` and returns `Any`,
# so we use that directly, skipping `__init__` validation.
reveal_type(Foo()) # revealed: Any
reveal_type(Foo("wrong")) # revealed: Any
__call__ returning NeverWhen metaclass __call__ returns Never, construction is terminal. We use metaclass __call__
directly and skip __new__ and __init__.
from typing_extensions import Never
class Meta(type):
def __call__(cls) -> Never:
raise NotImplementedError
class C(metaclass=Meta):
def __new__(cls, x: int) -> "C":
return object.__new__(cls)
def __init__(self, x: int) -> None:
pass
# `__new__` and `__init__` are skipped because metaclass `__call__` never returns.
reveal_type(C()) # revealed: Never
__call__ with mixed return typesWhen a metaclass __call__ is overloaded and some overloads return the class instance type while
others return a different type, non-instance-returning overloads use the metaclass __call__
directly, while instance-returning overloads are replaced by __init__ validation.
from typing import Any, overload
from typing_extensions import Literal
class Meta(type):
@overload
def __call__(cls, x: int) -> int: ...
@overload
def __call__(cls, x: str) -> "Foo": ...
def __call__(cls, x: int | str) -> Any:
return super().__call__(x)
class Foo(metaclass=Meta):
def __init__(self) -> None:
pass
# The `int` overload from the metaclass `__call__` is selected; its return type
# is not an instance of `Foo`, so it is used directly.
reveal_type(Foo(1)) # revealed: int
# The `str -> Foo` metaclass overload matches and returns an instance, so `__init__`
# is also validated.
# error: [too-many-positional-arguments]
reveal_type(Foo("hello")) # revealed: Foo
# No overload matches.
# error: [no-matching-overload]
reveal_type(Foo()) # revealed: Unknown
def _(a: Any):
# error: [too-many-positional-arguments]
reveal_type(Foo(a)) # revealed: Unknown
__call__ overloads should not become declaration-order dependentReversing the declaration order of the same mixed overload set should not change the result when
overload resolution falls back to Unknown.
from typing import Any, TypeVar, overload
from missing import Unknown # type: ignore
T = TypeVar("T")
class ReverseMeta(type):
@overload
def __call__(cls: type[T], x: str) -> str: ...
@overload
def __call__(cls: type[T], x: int) -> T: ...
def __call__(cls, x: int | str) -> object:
return super().__call__()
class ReverseMetaTarget(metaclass=ReverseMeta):
def __init__(self) -> None: ...
def _(a: Any, u: Unknown):
# error: [too-many-positional-arguments]
reveal_type(ReverseMetaTarget(a)) # revealed: Unknown
# error: [too-many-positional-arguments]
reveal_type(ReverseMetaTarget(u)) # revealed: Unknown
__call__ preserving strict-subclass returnfrom typing import Any, overload
class Meta(type):
@overload
def __call__(cls, x: int) -> int: ...
@overload
def __call__(cls, x: str) -> "Child": ...
def __call__(cls, x: int | str) -> Any:
return super().__call__(x)
class Parent(metaclass=Meta):
def __init__(self, x: str) -> None:
pass
class Child(Parent): ...
reveal_type(Parent(1)) # revealed: int
reveal_type(Parent("hello")) # revealed: Child
__call__ returning only non-instance typesWhen all overloads of a metaclass __call__ return non-instance types, the metaclass fully
overrides type.__call__ and __init__ is not checked.
from typing import Any, overload
class Meta(type):
@overload
def __call__(cls, x: int) -> int: ...
@overload
def __call__(cls, x: str) -> str: ...
def __call__(cls, x: int | str) -> Any:
return x
class Bar(metaclass=Meta):
def __init__(self, x: int, y: int) -> None:
pass
# `__init__` is not checked: it requires two `int` args, but we only pass one.
# No error is raised because the metaclass `__call__` controls construction.
reveal_type(Bar(1)) # revealed: int
reveal_type(Bar("hello")) # revealed: str
__call__ should not invent an instance returnIf no overload matches, we should still report Unknown rather than falling back to the class
instance type.
from typing import overload
class OnlyNonInstanceMeta(type):
@overload
def __call__(cls, x: int) -> int: ...
@overload
def __call__(cls, x: str) -> str: ...
def __call__(cls, x: int | str) -> object:
raise NotImplementedError
class OnlyNonInstanceMetaTarget(metaclass=OnlyNonInstanceMeta):
pass
# error: [no-matching-overload]
reveal_type(OnlyNonInstanceMetaTarget(1.2)) # revealed: Unknown
__call__ with non-class return formsWhen all overloads return non-instance types that aren't simple class instances (e.g., Callable),
__init__ should still be skipped.
from typing import Any, Callable, overload
class Meta(type):
@overload
def __call__(cls, x: int) -> Callable[[], int]: ...
@overload
def __call__(cls, x: str) -> Callable[[], str]: ...
def __call__(cls, x: int | str) -> Any:
return lambda: x
class Baz(metaclass=Meta):
def __init__(self, x: int, y: int) -> None:
pass
# `__init__` is not checked: it requires two `int` args, but we only pass one.
# No error is raised because the metaclass `__call__` controls construction.
reveal_type(Baz(1)) # revealed: () -> int
reveal_type(Baz("hello")) # revealed: () -> str
__call__ fails, __new__ is irrelevantclass Meta(type):
def __call__(cls, x: str) -> "C":
raise NotImplementedError
class C(metaclass=Meta):
def __new__(cls, x: bytes) -> int:
return 1
# error: [invalid-argument-type]
reveal_type(C(b"hello")) # revealed: C
__call__ is not a simple methodclass MetaCall:
def __call__(self) -> int:
return 1
class Meta(type):
__call__: MetaCall = MetaCall()
class C(metaclass=Meta): ...
reveal_type(C()) # revealed: int
__new__If metaclass __call__ forwards to normal construction by returning the constructed instance type,
and the downstream overloaded __new__ doesn't match, we error, but still assume the class instance
type.
from typing import TypeVar, overload
T = TypeVar("T")
class Meta(type):
def __call__(cls: type[T], x: object) -> T:
raise NotImplementedError
class D(metaclass=Meta):
@overload
def __new__(cls, x: int) -> int: ...
@overload
def __new__(cls, x: str) -> str: ...
def __new__(cls, x: object) -> object:
raise NotImplementedError
# error: [no-matching-overload]
reveal_type(D(1.2)) # revealed: D
__new__ and mixed metaclass __call__If both metaclass __call__ and __new__ are mixed (some overloads instance-returning and some
non-instance), the fallback chain works as expected: __new__ is only considered if metaclass
__call__ is instance-returning, and __init__ is only considered if both __call__ and __new__
are instance-returning.
from __future__ import annotations
from typing import Any, Literal, overload
class A: ...
class B: ...
class C: ...
class D: ...
class Meta(type):
@overload
def __call__(cls, x: A) -> A: ...
@overload
def __call__(cls, x: B) -> Test: ...
@overload
def __call__(cls, x: C) -> Test: ...
@overload
def __call__(cls, x: str) -> Test: ...
def __call__(cls, x: A | B | C | str) -> A | Test:
raise NotImplementedError()
class Test(metaclass=Meta):
@overload
def __new__(cls, x: B) -> B: ...
@overload
def __new__(cls, x: D) -> D: ...
@overload
def __new__(cls, x: str) -> Test: ...
def __new__(cls, x: B | D | str) -> B | D | Test:
raise NotImplementedError()
def __init__(self, x: Literal["ok"]) -> None:
pass
# `A` matches the first metaclass overload, which returns `A`, bypassing `__new__` and `__init__`
# since `A` is not a subtype of `Test`.
reveal_type(Test(A())) # revealed: A
# `B` returns `Test` from metaclass `__call__` and returns `B` from `__new__`, bypassing `__init__`
# since `B` is not a subtype of `Test`.
reveal_type(Test(B())) # revealed: B
# `C` returns `Test` from metaclass `__call__` and fails the call to `__new__`.
# error: [no-matching-overload]
reveal_type(Test(C())) # revealed: Test
# `D` fails metaclass `__call__`, so never reaches `__new__` or `__init__`, and we infer `Unknown`
# since not all overloads are instance-returning.
# error: [no-matching-overload]
reveal_type(Test(D())) # revealed: Unknown
# `str` returns `Test` from both `__call__` and `__new__`, but `__init__` rejects `Literal["bad"]`.
# error: [invalid-argument-type]
reveal_type(Test("bad")) # revealed: Test
# `Literal["ok"]` returns `Test` from both `__call__` and `__new__`, and is accepted by `__init__`.
reveal_type(Test("ok")) # revealed: Test
class M(type): ...
reveal_type(M.__class__) # revealed: <class 'type'>
objectreveal_type(object.__class__) # revealed: <class 'type'>
typereveal_type(type.__class__) # revealed: <class 'type'>
class M(type): ...
class B(metaclass=M): ...
reveal_type(B.__class__) # revealed: <class 'M'>
A class which doesn't inherit type (and/or doesn't implement a custom __new__ accepting the same
arguments as type.__new__) isn't a valid metaclass.
class M: ...
class A(metaclass=M): ...
# TODO: emit a diagnostic for the invalid metaclass
reveal_type(A.__class__) # revealed: <class 'M'>
If a class is a subclass of a class with a custom metaclass, then the subclass will also have that metaclass.
class M(type): ...
class A(metaclass=M): ...
class B(A): ...
reveal_type(B.__class__) # revealed: <class 'M'>
The same is true if the base with the metaclass is a generic class.
[environment]
python-version = "3.13"
class M(type): ...
class A[T](metaclass=M): ...
class B(A): ...
class C(A[int]): ...
reveal_type(B.__class__) # revealed: <class 'M'>
reveal_type(C.__class__) # revealed: <class 'M'>
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a subclass or the class itself.)
class M1(type): ...
class M2(type): ...
class A(metaclass=M1): ...
class B(metaclass=M2): ...
# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
class C(A, B): ...
reveal_type(C.__class__) # revealed: type[Unknown]
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a subclass or the class itself.)
class M1(type): ...
class M2(type): ...
class A(metaclass=M1): ...
# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship"
class B(A, metaclass=M2): ...
reveal_type(B.__class__) # revealed: type[Unknown]
A class has two explicit bases, both of which have the same metaclass.
class M(type): ...
class A(metaclass=M): ...
class B(metaclass=M): ...
class C(A, B): ...
reveal_type(C.__class__) # revealed: <class 'M'>
A class has an explicit base with a custom metaclass. That metaclass itself has a custom metaclass.
class M1(type): ...
class M2(type, metaclass=M1): ...
class M3(M2): ...
class A(metaclass=M3): ...
class B(A): ...
reveal_type(A.__class__) # revealed: <class 'M3'>
class M(type): ...
class M1(M): ...
class M2(M): ...
class M12(M1, M2): ...
class A(metaclass=M1): ...
class B(metaclass=M2): ...
class C(metaclass=M12): ...
# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
class D(A, B, C): ...
reveal_type(D.__class__) # revealed: type[Unknown]
from nonexistent_module import UnknownClass # error: [unresolved-import]
class C(UnknownClass): ...
# TODO: should be `type[type] & Unknown`
reveal_type(C.__class__) # revealed: <class 'type'>
class M(type): ...
class A(metaclass=M): ...
class B(A, UnknownClass): ...
# TODO: should be `type[M] & Unknown`
reveal_type(B.__class__) # revealed: <class 'M'>
class M(type): ...
class A(metaclass=M): ...
class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`"
reveal_type(B.__class__) # revealed: <class 'M'>
When a class has an explicit metaclass that is not a class, but is a callable that accepts
type.__new__ arguments, we should return the meta-type of its return type.
def f(*args, **kwargs) -> int:
return 1
class A(metaclass=f): ...
# TODO: Should be `int`
reveal_type(A) # revealed: <class 'A'>
reveal_type(A.__class__) # revealed: type[int]
def _(n: int):
# error: [invalid-metaclass]
class B(metaclass=n): ...
# TODO: Should be `Unknown`
reveal_type(B) # revealed: <class 'B'>
reveal_type(B.__class__) # revealed: type[Unknown]
def _(flag: bool):
m = f if flag else 42
# error: [invalid-metaclass]
class C(metaclass=m): ...
# TODO: Should be `int | Unknown`
reveal_type(C) # revealed: <class 'C'>
reveal_type(C.__class__) # revealed: type[Unknown]
class SignatureMismatch: ...
# TODO: Emit a diagnostic
class D(metaclass=SignatureMismatch): ...
# TODO: Should be `Unknown`
reveal_type(D) # revealed: <class 'D'>
# TODO: Should be `type[Unknown]`
reveal_type(D.__class__) # revealed: <class 'SignatureMismatch'>
def _(n: int):
# snapshot: invalid-metaclass
class B(metaclass=n):
x = 1
y = 2
error[invalid-metaclass]: Metaclass type `int` is not callable
--> src/mdtest_snippet.py:3:13
|
3 | class B(metaclass=n):
| ^^^^^^^^^^^
|
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
class A(B): ... # error: [cyclic-class-definition]
class B(C): ... # error: [cyclic-class-definition]
class C(A): ... # error: [cyclic-class-definition]
reveal_type(A.__class__) # revealed: type[Unknown]
[environment]
python-version = "3.12"
class M(type): ...
class A[T: str](metaclass=M): ...
reveal_type(A.__class__) # revealed: <class 'M'>
A generic metaclass fully specialized with concrete types is fine:
[environment]
python-version = "3.13"
class Foo[T](type):
x: T
class Bar(metaclass=Foo[int]): ...
reveal_type(Bar.__class__) # revealed: <class 'Foo[int]'>
A generic metaclass parameterized by type variables is not supported:
from typing import TypeVar, Generic
T = TypeVar("T")
class GenericMeta(type, Generic[T]): ...
# error: [invalid-metaclass] "Generic metaclasses are not supported"
class GenericMetaInstance(metaclass=GenericMeta[T]): ...
The same applies using PEP 695 syntax:
[environment]
python-version = "3.13"
class Foo[T](type):
x: T
# error: [invalid-metaclass]
class Bar[T](metaclass=Foo[T]): ...
class Foo(type): ...
class Bar(type, metaclass=Foo): ...
class Baz(type, metaclass=Bar): ...
class Spam(metaclass=Baz): ...
reveal_type(Spam.__class__) # revealed: <class 'Baz'>
reveal_type(Spam.__class__.__class__) # revealed: <class 'Bar'>
reveal_type(Spam.__class__.__class__.__class__) # revealed: <class 'Foo'>
def test(x: Spam):
reveal_type(x.__class__) # revealed: type[Spam]
reveal_type(x.__class__.__class__) # revealed: type[Baz]
reveal_type(x.__class__.__class__.__class__) # revealed: type[Bar]
reveal_type(x.__class__.__class__.__class__.__class__) # revealed: type[Foo]
reveal_type(x.__class__.__class__.__class__.__class__.__class__) # revealed: type[type]
# revealed: type[type]
reveal_type(x.__class__.__class__.__class__.__class__.__class__.__class__.__class__.__class__)