11 KiB
Custom __call__ on metaclass
When a metaclass defines a custom __call__ method, it controls what happens when the class is
called. The return type and parameter types of the metaclass __call__ are used instead of the
class's __new__ and __init__ methods.
Basic metaclass __call__
class 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
Metaclass __call__ with wrong arguments
class Meta(type):
def __call__(cls, x: int) -> int:
return x
class Foo(metaclass=Meta): ...
Foo("wrong") # error: [invalid-argument-type]
Foo() # error: [missing-argument]
Foo(1, 2) # error: [too-many-positional-arguments]
Metaclass __call__ takes precedence over __init__
class Meta(type):
def __call__(cls) -> str:
return "hello"
class Foo(metaclass=Meta):
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
Metaclass __call__ with TypeVar return type
When 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
Metaclass __call__ with no return type annotation
When the metaclass __call__ has no return type annotation (returns Unknown), we should still
check the __new__ and __init__ signatures.
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
Metaclass __call__ with specific parameters
When the metaclass __call__ has specific parameters (not just *args, **kwargs), we should check
the metaclass __call__ signature, even if the return type is the instance type.
from typing import TypeVar
T = TypeVar("T")
class Meta(type):
def __call__(cls: type[T], x: int) -> T:
return object.__new__(cls)
class Foo(metaclass=Meta):
def __init__(self, x: int) -> None:
pass
# The metaclass __call__ has specific parameters, so we check them.
Foo("wrong") # error: [invalid-argument-type]
Foo() # error: [missing-argument]
reveal_type(Foo(1)) # revealed: Foo
Metaclass __call__ returning bare type
When the metaclass __call__ is annotated as returning type, this is typically a mistake in
singleton patterns where the programmer intended to return an instance. Both mypy and pyright handle
this specially by ignoring the type return annotation and using the instance type instead.
from typing import Any
class Singleton(type):
_instances: dict["Singleton", object] = {}
def __call__(cls, *args: Any, **kwargs: Any) -> type:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
# error: [invalid-return-type]
return cls._instances[cls]
class MyConfig(metaclass=Singleton):
def __init__(self, x: int) -> None:
pass
def get(self, key: str) -> str:
return key
# Despite the `-> type` annotation, we treat this as returning an instance.
# This matches mypy and pyright behavior for this common pattern.
MyConfig() # error: [missing-argument]
reveal_type(MyConfig(1)) # revealed: MyConfig
# Instance methods work correctly.
MyConfig(1).get("key")
Default
class M(type): ...
reveal_type(M.__class__) # revealed: <class 'type'>
object
reveal_type(object.__class__) # revealed: <class 'type'>
type
reveal_type(type.__class__) # revealed: <class 'type'>
Basic
class M(type): ...
class B(metaclass=M): ...
reveal_type(B.__class__) # revealed: <class 'M'>
Invalid metaclass
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'>
Linear inheritance
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'>
Linear inheritance with PEP 695 generic class
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'>
Conflict (1)
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]
Conflict (2)
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]
Common metaclass
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'>
Metaclass metaclass
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'>
Diamond inheritance
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]
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'>
Duplicate
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'>
Non-class
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'>
Cyclic
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]
PEP 695 generic
[environment]
python-version = "3.12"
class M(type): ...
class A[T: str](metaclass=M): ...
reveal_type(A.__class__) # revealed: <class 'M'>
Metaclasses of metaclasses
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__)