Ok, obviously __new__
in a metaclass runs when an instance of the metaclass i.e. a class object is instantiated, so __new__
in a metaclass provides a hook to intercept events (/code that runs) at class definition time (e.g. validating/enforcing rules for class attributes such as methods etc.).
Many online examples of __new__
in a metaclass return an instance of the type constructor from __new__
, which seems a bit problematic since this blocks __init__
(docs: "If __new__()
does not return an instance of cls, then the new instance’s __init__()
method will not be invoked").
While tinkering with return values of __new__
in a metaclass I came across some somewhat strange cases which I do not fully understand, e.g.:
class Meta(type):
def __new__(self, name, bases, attrs):
print("Meta __new__ running!")
# return type(name, bases, attrs) # 1.
# return super().__new__(self, name, bases, attrs) # 2.
# return super().__new__(name, bases, attrs) # 3.
# return super().__new__(type, name, bases, attrs) # 4.
# return self(name, bases, attrs) # 5.
def __init__(self, *args, **kwargs):
print("Meta __init__ running!")
return super().__init__(*args, **kwargs)
class Cls(metaclass=Meta):
pass
- This is often seen in examples and generally works, but blocks
__init__
- This works and
__init__
also fires; but why pass self to a super() call? Shouldn't self/cls get passed automatically with super()? - This throws a somewhat strange error I can't really make sense of: TypeError:
type.__new__(X)
: X is not a type object (str); what is X? shouldn't self be auto-passed? - The error of 3. inspired me to play with the first arg of the super() call, so I tried to pass type directly; this also blocks
__init__
. What is happening here? - tried just for fun; this leads to a RecursionError
Also especially cases 1. and 2. appear to have quite profound implications for inheriting from classes bound to metaclasses:
class LowerCaseEnforcer(type):
""" Allows only lower case names as class attributes! """
def __new__(self, name, bases, attrs):
for name in attrs:
if name.lower() != name:
raise TypeError(f"Inappropriate method name: {name}")
# return type(name, bases, attrs) # 1.
# return super().__new__(self, name, bases, attrs) # 2.
class Super(metaclass=LowerCaseEnforcer):
pass
class Sub(Super):
def some_method(self):
pass
## this will error in case 2 but not case 1
def Another_method(self):
pass
- expected behavior: metaclass is bound to superclass, but not to subclass
- binds the superclass /and/ subclasses to the metaclass; ?
I would much appreciate if someone could slowly and kindly explain what exactly is going on in the above examples! :D