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
31 changes: 31 additions & 0 deletions docs/source/metaclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,34 @@ 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 assumes that their metaclass is :py:class:`abc.ABCMeta`
even if it's :py:class:`type`. 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]``, or
* compute the metaclass' superclass dynamically, which mypy doesn't understand
so it will also need to be muted.

.. code-block:: python

import abc

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

# the problem:
class M0(type): pass
class A0(tuple, metaclass=M1): 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]

# option 3: compute the metaclass dynamically
class M3(type(tuple)): pass # type: ignore[metaclass]
class A3(tuple, metaclass=M3): pass
6 changes: 5 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2955,10 +2955,14 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
if typ.metaclass_type is None and any(
base.type.metaclass_type is not None for base in typ.bases
):
explanation = typ.explain_metaclass_conflict() or ""
if explanation:
explanation = f" - {explanation}"
self.fail(
"Metaclass conflict: the metaclass of a derived class must be "
"a (non-strict) subclass of the metaclasses of all its bases",
f"a (non-strict) subclass of the metaclasses of all its bases{explanation}",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm aware that adding the explanation makes error message differ from the runtime error. However, since type hints sometimes don't follow the actual runtime metaclasses (s. issue this PR is supposed fix), I think this information can be valuable.

Copy link
Member

Choose a reason for hiding this comment

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

Mypy errors don't necessarily have to mirror runtime errors exactly. It's nice to be consistent, but maybe we have more information so we can give a more detailed error, or we think a different wording is more informative.

typ,
code=codes.METACLASS,
)

def visit_import_from(self, node: ImportFrom) -> None:
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
36 changes: 36 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3402,6 +3402,42 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None:

return winner

def explain_metaclass_conflict(self) -> str | None:
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} (meta 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)} conflicting 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
22 changes: 11 additions & 11 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4757,7 +4757,7 @@ 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 - __main__.Y (meta of __main__.B) conflicting with __main__.X (metaclass of __main__.A)

[case testMetaclassNoTypeReveal]
class M:
Expand Down Expand Up @@ -5754,8 +5754,8 @@ 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 - __main__.M (meta of __main__.CQA) conflicting 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 - __main__.M (meta of __main__.CQW) conflicting with __main__.M1 (metaclass of __main__.Q1)
[builtins fixtures/tuple.pyi]

[case testSixMetaclassAny]
Expand Down Expand Up @@ -5873,7 +5873,7 @@ 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 - __main__.M (meta of __main__.CQW) conflicting with __main__.M1 (metaclass of __main__.Q1)
[builtins fixtures/tuple.pyi]

[case testFutureMetaclassAny]
Expand Down Expand Up @@ -7342,17 +7342,17 @@ 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 - __main__.MyMeta1 (metaclass of __main__.A) conflicting 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 - __main__.MyMeta1 (metaclass of __main__.A) conflicting 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 - __main__.MyMeta2 (metaclass of __main__.B) conflicting 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 - __main__.ConflictingMeta (meta of __main__.Conflict4) conflicting 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 - __main__.ConflictingMeta (meta of __main__.ChildOfCorrectButWrongMeta) conflicting with __main__.CorrectMeta (metaclass of __main__.CorrectSubclass1)
...

[case testMetaClassConflictIssue14033]
Expand All @@ -7367,8 +7367,8 @@ 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 - __main__.M1 (metaclass of __main__.A1) conflicting 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 - __main__.M2 (metaclass of __main__.B1) conflicting with __main__.M1 (metaclass of __main__.A1)

# should not warn again for children
class ChildOfTestABC(TestABC): pass
Expand Down
37 changes: 37 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,43 @@ 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 - __main__.Y (meta of __main__.B) conflicting with __main__.X (metaclass of __main__.A) [metaclass]

[case testOverloadedFunctionSignature]
from typing import overload, Union
Expand Down
12 changes: 8 additions & 4 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,29 @@ 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: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - a.M2 (meta of a.D) conflicting 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: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - b.M2 (meta of a.D) conflicting with b.M (metaclass of a.B)

[case testFineMetaclassRemoveFromClass]
import a
Expand Down