Python Metaclass Inheritance Pitfalls: When C and Python Metaclasses Collide
Combining C and Python metaclasses triggers TypeError when C tp_new uses MRO to invoke Python __new__. Constraints: safe tp_new chaining and tp_basicsize. Fixes: reorder bases (Python metaclass first) or modify C tp_new to call tp_base->tp_new (skips Python __new__). Increasing tp_basicsize ensures correct base selection. First reported 2004, affects ZODB, SQLAlchemy; a silent hazard. Key takeaway: never let C tp_new invoke Python __new__; prefer composition; document tp_basicsize requirement...
The Bottom Line
Combining C-defined and Python-defined metaclasses in Python can trigger a TypeError: type.__new__(MetaTest) is not safe due to unsafe method resolution in tp_new. This occurs when a C metaclass uses MRO to call a Python __new__ method. Two structural constraints — safe tp_new chaining and tp_basicsize requirements — govern whether such combinations will work. Reordering bases or modifying C extension code can resolve it.
Key Insight: C-level
tp_newfunctions must never invoke Python-level__new__methods. Always calltp_base->tp_newdirectly for safe cooperative inheritance.
The Problem
When defining a class that inherits from multiple metaclasses — one implemented in C (CMeta) and one in Python (PyMeta) — instance creation fails with:
TypeError: type.__new__(MetaTest) is not safe, use CMeta.__new__()
This happens because the C metaclass’s tp_new uses method resolution order (MRO) to find the next constructor, which resolves to the Python __new__ method. The Python runtime prevents this for safety: C code should not invoke arbitrary Python __new__ implementations.
Failing Code Example
class PyMeta(type):
def __new__(meta, name, bases, attrs):
return super(PyMeta, meta).__new__(meta, name, bases, attrs)
class MetaTest(CMeta, PyMeta): # CMeta first
pass
class Test(metaclass=MetaTest):
pass
# This raises TypeError
obj = Test()
The error occurs during the super() call in PyMeta.__new__, which is reached via CMeta.tp_new’s MRO walk.
The Fix
Option 1: Reorder Metaclass Bases
Place the Python metaclass before the C metaclass:
class MetaTest(PyMeta, CMeta): # PyMeta first
pass
This works because MRO now starts with PyMeta, and if CMeta.tp_new doesn’t attempt to call further __new__ methods, the chain terminates safely. However, this is fragile — if CMeta.tp_new still walks MRO, the error may persist.
Option 2: Modify C tp_new Implementation
Instead of using dynamic MRO, have CMeta.tp_new call:
type->tp_base->tp_new(...);
This bypasses MRO entirely and safely chains to the next C-level tp_new. However, this skips any Python __new__ in the hierarchy, breaking cooperative inheritance.
Option 3: Increase tp_basicsize
Ensure your C type has a larger tp_basicsize than its base:
tp_basicsize > base_type->tp_basicsize
This guarantees it will be selected as the __base__ even if not first in __bases__, avoiding the MRO hazard.
Why It Matters
This edge case affects:
- C extension developers who allow subclassing of their types from Python
- Framework authors using metaclass composition
- ZODB, SQLAlchemy, and similar ORM tools that define C-backed types
Without understanding these rules, developers may ship extensions that break when combined with Python metaclasses — failing only in multiple inheritance scenarios that are hard to test.
The issue was first reported in 2004 (BPO #963246) and remains in CPython’s behavior. While rare, it’s a silent correctness hazard — code works in isolation but fails when composed.
Key Takeaway
- Never let C
tp_newinvoke Python__new__— always usetp_base->tp_new - Document
tp_basicsizerequirements for any C type meant to be inherited - Test metaclass composition — especially with mixed C/Python hierarchies
- Prefer composition over multiple metaclass inheritance when possible
This case exemplifies the tension between Python’s dynamic dispatch and C extension safety. The runtime’s protection prevents crashes but surfaces as cryptic TypeErrors — making diagnosis hard without understanding the C API rules.