Skip to content
Merged
29 changes: 29 additions & 0 deletions docs/source/error_code_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,35 @@ You can use :py:class:`~collections.abc.Callable` as the type for callable objec
for x in objs:
f(x)

.. _code-metaclass:

Check the validity of a class's metaclass [metaclass]
-----------------------------------------------------

Mypy checks whether the metaclass of a class is valid. The metaclass
must be a subclass of ``type``. Further, the class hierarchy must yield
a consistent metaclass. For more details, see the
`Python documentation <https://docs.python.org/3.13/reference/datamodel.html#determining-the-appropriate-metaclass>`_

Note that mypy's metaclass checking is limited and may produce false-positives.
See also :ref:`limitations`.

Example with an error:

.. code-block:: python

class GoodMeta(type):
pass

class BadMeta:
pass

class A1(metaclass=GoodMeta): # OK
pass

class A2(metaclass=BadMeta): # Error: Metaclasses not inheriting from "type" are not supported [metaclass]
pass

.. _code-var-annotated:

Require annotation if variable type is unclear [var-annotated]
Expand Down
25 changes: 25 additions & 0 deletions docs/source/metaclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,28 @@ so it's better not to combine metaclasses and class hierarchies:
* ``Self`` is not allowed as annotation in metaclasses as per `PEP 673`_.

.. _PEP 673: https://peps.python.org/pep-0673/#valid-locations-for-self

For some builtin types, mypy may think their metaclass is :py:class:`abc.ABCMeta`
even if it is :py:class:`type` at runtime. In those cases, you can either:

* use :py:class:`abc.ABCMeta` instead of :py:class:`type` as the
superclass of your metaclass if that works in your use-case
* mute the error with ``# type: ignore[metaclass]``

.. code-block:: python

import abc

assert type(tuple) is type # metaclass of tuple is type at runtime

# The problem:
class M0(type): pass
class A0(tuple, metaclass=M0): pass # Mypy Error: metaclass conflict

# Option 1: use ABCMeta instead of type
class M1(abc.ABCMeta): pass
class A1(tuple, metaclass=M1): pass

# Option 2: mute the error
class M2(type): pass
class A2(tuple, metaclass=M2): pass # type: ignore[metaclass]
4 changes: 4 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2959,7 +2959,11 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
"Metaclass conflict: the metaclass of a derived class must be "
"a (non-strict) subclass of the metaclasses of all its bases",
typ,
code=codes.METACLASS,
)
explanation = typ.explain_metaclass_conflict()
if explanation:
self.note(explanation, typ, code=codes.METACLASS)

def visit_import_from(self, node: ImportFrom) -> None:
for name, _ in node.names:
Expand Down
1 change: 1 addition & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def __hash__(self) -> int:
"General",
default_enabled=False,
)
METACLASS: Final[ErrorCode] = ErrorCode("metaclass", "Ensure that metaclass is valid", "General")

# Syntax errors are often blocking.
SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General")
Expand Down
37 changes: 37 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3402,6 +3402,43 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None:

return winner

def explain_metaclass_conflict(self) -> str | None:
# Compare to logic in calculate_metaclass_type
declared = self.declared_metaclass
if declared is not None and not declared.type.has_base("builtins.type"):
return None
if self._fullname == "builtins.type":
return None

winner = declared
if declared is None:
resolution_steps = []
else:
resolution_steps = [f'"{declared.type.fullname}" (metaclass of "{self.fullname}")']
for super_class in self.mro[1:]:
super_meta = super_class.declared_metaclass
if super_meta is None or super_meta.type is None:
continue
if winner is None:
winner = super_meta
resolution_steps.append(
f'"{winner.type.fullname}" (metaclass of "{super_class.fullname}")'
)
continue
if winner.type.has_base(super_meta.type.fullname):
continue
if super_meta.type.has_base(winner.type.fullname):
winner = super_meta
resolution_steps.append(
f'"{winner.type.fullname}" (metaclass of "{super_class.fullname}")'
)
continue
# metaclass conflict
conflict = f'"{super_meta.type.fullname}" (metaclass of "{super_class.fullname}")'
return f"{' > '.join(resolution_steps)} conflicts with {conflict}"

return None

def is_metaclass(self, *, precise: bool = False) -> bool:
return (
self.has_base("builtins.type")
Expand Down
17 changes: 13 additions & 4 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2702,7 +2702,7 @@ def infer_metaclass_and_bases_from_compat_helpers(self, defn: ClassDef) -> None:
if len(metas) == 0:
return
if len(metas) > 1:
self.fail("Multiple metaclass definitions", defn)
self.fail("Multiple metaclass definitions", defn, code=codes.METACLASS)
return
defn.metaclass = metas.pop()

Expand Down Expand Up @@ -2758,7 +2758,11 @@ def get_declared_metaclass(
elif isinstance(metaclass_expr, MemberExpr):
metaclass_name = get_member_expr_fullname(metaclass_expr)
if metaclass_name is None:
self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr)
self.fail(
f'Dynamic metaclass not supported for "{name}"',
metaclass_expr,
code=codes.METACLASS,
)
return None, False, True
sym = self.lookup_qualified(metaclass_name, metaclass_expr)
if sym is None:
Expand All @@ -2769,6 +2773,7 @@ def get_declared_metaclass(
self.fail(
f'Class cannot use "{sym.node.name}" as a metaclass (has type "Any")',
metaclass_expr,
code=codes.METACLASS,
)
return None, False, True
if isinstance(sym.node, PlaceholderNode):
Expand All @@ -2786,11 +2791,15 @@ def get_declared_metaclass(
metaclass_info = target.type

if not isinstance(metaclass_info, TypeInfo) or metaclass_info.tuple_type is not None:
self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr)
self.fail(
f'Invalid metaclass "{metaclass_name}"', metaclass_expr, code=codes.METACLASS
)
return None, False, False
if not metaclass_info.is_metaclass():
self.fail(
'Metaclasses not inheriting from "type" are not supported', metaclass_expr
'Metaclasses not inheriting from "type" are not supported',
metaclass_expr,
code=codes.METACLASS,
)
return None, False, False
inst = fill_typevars(metaclass_info)
Expand Down
38 changes: 24 additions & 14 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4757,8 +4757,8 @@ class C(B):
class X(type): pass
class Y(type): pass
class A(metaclass=X): pass
class B(A, metaclass=Y): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

class B(A, metaclass=Y): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.Y" (metaclass of "__main__.B") conflicts with "__main__.X" (metaclass of "__main__.A")
[case testMetaclassNoTypeReveal]
class M:
x = 0 # type: int
Expand Down Expand Up @@ -5737,8 +5737,8 @@ def f() -> type: return M
class C1(six.with_metaclass(M), object): pass # E: Unsupported dynamic base class "six.with_metaclass"
class C2(C1, six.with_metaclass(M)): pass # E: Unsupported dynamic base class "six.with_metaclass"
class C3(six.with_metaclass(A)): pass # E: Metaclasses not inheriting from "type" are not supported
@six.add_metaclass(A) # E: Metaclasses not inheriting from "type" are not supported \
# E: Argument 1 to "add_metaclass" has incompatible type "type[A]"; expected "type[type]"
@six.add_metaclass(A) # E: Metaclasses not inheriting from "type" are not supported \
# E: Argument 1 to "add_metaclass" has incompatible type "type[A]"; expected "type[type]"

class D3(A): pass
class C4(six.with_metaclass(M), metaclass=M): pass # E: Multiple metaclass definitions
Expand All @@ -5754,8 +5754,10 @@ class CD(six.with_metaclass(M)): pass # E: Multiple metaclass definitions
class M1(type): pass
class Q1(metaclass=M1): pass
@six.add_metaclass(M)
class CQA(Q1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class CQW(six.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class CQA(Q1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.M" (metaclass of "__main__.CQA") conflicts with "__main__.M1" (metaclass of "__main__.Q1")
class CQW(six.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.M" (metaclass of "__main__.CQW") conflicts with "__main__.M1" (metaclass of "__main__.Q1")
[builtins fixtures/tuple.pyi]

[case testSixMetaclassAny]
Expand Down Expand Up @@ -5873,7 +5875,8 @@ class C5(future.utils.with_metaclass(f())): pass # E: Dynamic metaclass not sup

class M1(type): pass
class Q1(metaclass=M1): pass
class CQW(future.utils.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class CQW(future.utils.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.M" (metaclass of "__main__.CQW") conflicts with "__main__.M1" (metaclass of "__main__.Q1")
[builtins fixtures/tuple.pyi]

[case testFutureMetaclassAny]
Expand Down Expand Up @@ -7342,17 +7345,22 @@ class ChildOfCorrectSubclass1(CorrectSubclass1): ...
class CorrectWithType1(C, A1): ...
class CorrectWithType2(B, C): ...

class Conflict1(A1, B, E): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class Conflict2(A, B): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class Conflict3(B, A): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class Conflict1(A1, B, E): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.MyMeta1" (metaclass of "__main__.A") conflicts with "__main__.MyMeta2" (metaclass of "__main__.B")
class Conflict2(A, B): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.MyMeta1" (metaclass of "__main__.A") conflicts with "__main__.MyMeta2" (metaclass of "__main__.B")
class Conflict3(B, A): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.MyMeta2" (metaclass of "__main__.B") conflicts with "__main__.MyMeta1" (metaclass of "__main__.A")

class ChildOfConflict1(Conflict3): ...
class ChildOfConflict2(Conflict3, metaclass=CorrectMeta): ...

class ConflictingMeta(MyMeta1, MyMeta3): ...
class Conflict4(A1, B, E, metaclass=ConflictingMeta): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class Conflict4(A1, B, E, metaclass=ConflictingMeta): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.ConflictingMeta" (metaclass of "__main__.Conflict4") conflicts with "__main__.MyMeta2" (metaclass of "__main__.B")

class ChildOfCorrectButWrongMeta(CorrectSubclass1, metaclass=ConflictingMeta): # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class ChildOfCorrectButWrongMeta(CorrectSubclass1, metaclass=ConflictingMeta): # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.ConflictingMeta" (metaclass of "__main__.ChildOfCorrectButWrongMeta") conflicts with "__main__.CorrectMeta" (metaclass of "__main__.CorrectSubclass1")
...

[case testMetaClassConflictIssue14033]
Expand All @@ -7367,8 +7375,10 @@ class B1(metaclass=M2): pass

class C1(metaclass=Mx): pass

class TestABC(A2, B1, C1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class TestBAC(B1, A2, C1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
class TestABC(A2, B1, C1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.M1" (metaclass of "__main__.A1") conflicts with "__main__.M2" (metaclass of "__main__.B1")
class TestBAC(B1, A2, C1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
# N: "__main__.M2" (metaclass of "__main__.B1") conflicts with "__main__.M1" (metaclass of "__main__.A1")

# should not warn again for children
class ChildOfTestABC(TestABC): pass
Expand Down
41 changes: 41 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,47 @@ def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of inpu

[builtins fixtures/tuple.pyi]

[case testDynamicMetaclass]
class A(metaclass=type(tuple)): pass # E: Dynamic metaclass not supported for "A" [metaclass]
[builtins fixtures/tuple.pyi]

[case testMetaclassOfTypeAny]
# mypy: disallow-subclassing-any=True
from typing import Any
foo: Any = ...
class A(metaclass=foo): pass # E: Class cannot use "foo" as a metaclass (has type "Any") [metaclass]

[case testMetaclassOfWrongType]
class Foo:
bar = 1
class A2(metaclass=Foo.bar): pass # E: Invalid metaclass "Foo.bar" [metaclass]

[case testMetaclassNotTypeSubclass]
class M: pass
class A(metaclass=M): pass # E: Metaclasses not inheriting from "type" are not supported [metaclass]

[case testMultipleMetaclasses]
import six
class M1(type): pass

@six.add_metaclass(M1)
class A1(metaclass=M1): pass # E: Multiple metaclass definitions [metaclass]

class A2(six.with_metaclass(M1), metaclass=M1): pass # E: Multiple metaclass definitions [metaclass]

@six.add_metaclass(M1)
class A3(six.with_metaclass(M1)): pass # E: Multiple metaclass definitions [metaclass]
[builtins fixtures/tuple.pyi]

[case testInvalidMetaclassStructure]
class X(type): pass
class Y(type): pass
class A(metaclass=X): pass
class B(A, metaclass=Y): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [metaclass] \
# N: "__main__.Y" (metaclass of "__main__.B") conflicts with "__main__.X" (metaclass of "__main__.A")




[case testOverloadedFunctionSignature]
from typing import overload, Union
Expand Down
10 changes: 8 additions & 2 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -2936,10 +2936,12 @@ a.py:6: error: Argument 1 to "f" has incompatible type "type[B]"; expected "M"

[case testFineMetaclassRecalculation]
import a

[file a.py]
from b import B
class M2(type): pass
class D(B, metaclass=M2): pass

[file b.py]
import c
class B: pass
Expand All @@ -2949,27 +2951,31 @@ import c
class B(metaclass=c.M): pass

[file c.py]
class M(type):
pass
class M(type): pass
[out]
==
a.py:3: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
a.py:3: note: "a.M2" (metaclass of "a.D") conflicts with "c.M" (metaclass of "b.B")

[case testFineMetaclassDeclaredUpdate]
import a

[file a.py]
import b
class B(metaclass=b.M): pass
class D(B, metaclass=b.M2): pass

[file b.py]
class M(type): pass
class M2(M): pass

[file b.py.2]
class M(type): pass
class M2(type): pass
[out]
==
a.py:3: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
a.py:3: note: "b.M2" (metaclass of "a.D") conflicts with "b.M" (metaclass of "a.B")

[case testFineMetaclassRemoveFromClass]
import a
Expand Down