Motivation
In Python, attribute lookups of both a class and an instance follow MRO, a behavior that is widely considered both DRY and intuitive:
class Base:
foo = 'bar'
class Child(Base):
pass
print(Child.foo) # outputs bar
print(Child().foo) # outputs bar
And yet, within the definition of the child class, it often annoys/surprises me and many others that there is not an equivalent name resolution order:
class Base:
foo = 'bar'
class Child(Base):
bar = foo # raises NameError
The typical workaround is to get the attribute explicitly from the base class that has the attribute:
class Base:
foo = 'bar'
class Child(Base):
bar = Base.foo # bar = 'bar'
But it just feels inelegant in terms of DRY having to repeat the name of the base class everywhere in the subclass where an attribute of the base class is needed.
Many Python users also find the behavior counter-intuitive and the workaround inelegant, so here are my proposals to improve ergonomics and maintainability of definitions of child classes:
Proposal #1
Make type.__prepare__
return a custom namespace that would default to a ChainMap
of __dict__
s of base classes when a given name is not found in the current dict. The behavior is enabled only when a new given keyword argument inherit_namespace
is true.
So the logics of the new type
would be similar to:
from collections import ChainMap
class InheritedNamespace(dict):
def __init__(self, bases):
self.chainmap = ChainMap(*map(vars, bases))
def __getitem__(self, name):
try:
return super().__getitem__(name)
except KeyError:
return self.chainmap[name]
class NewType(type):
@classmethod
def __prepare__(metacls, name, bases, inherit_namespace=False):
if inherit_namespace:
return InheritedNamespace(bases)
return {}
def __new__(cls, name, bases, namespace, inherit_namespace=False):
return super().__new__(cls, name, bases, namespace)
so that:
class Base(metaclass=NewType):
foo = 'bar'
class Child(Base, inherit_namespace=True):
bar = foo
print(Child.bar) # outputs bar
Currently IDEs and type checkers would complain about foo
being undefined when referenced in the body of Child
in the above code, but should no longer do so once the change is made official.
Backward Compatibility
While the existing code will continue to work since the new behavior is opted in, it should be documented that since this new feature is implemented with a non-dict namespace class InheritedNamespace
, libraries that use metaclasses with custom namespaces are advised to make this new built-in namespace class a base class for their custom namespace class when inherit_namespace
is true so that their metaclasses are mergeable with other metaclasses.
Performance Impact
There should not be any performance impact to existing code as name lookups continue to be performed on a regular dict. When inherit_namespace
is true, there will be a small performace impact to a failing name lookup to produce an exception.
But WAIT!
On second thought, what meaningful downside could there possibly be by making an MRO-based fallback the default behavior for all name lookups within the definitions of all classes? Names that fail to be looked up in the class definition shouldn’t be in any of the existing code base, so applying the proposed behavior to all class definitions only enhances user experiences with what couldn’t be done before.
So here is an even better, less disruptive…
Proposal #2
Make a wrapper namespace over the namespace prepared by the metaclass (which defaults to an empty dict prepared by type
). The wrapper namespace would delegate all accesses to the actual namespace, but would fall back to a ChainMap
of __dict__
s of the base classes should a name not be found in the actual namespace:
from collections import ChainMap
class InheritedNamespace(dict):
def __init__(self, bases, namespace):
self.chainmap = ChainMap(*map(vars, bases))
self.namespace = namespace
def __getitem__(self, name):
try:
return self.namespace[name]
except KeyError:
return self.chainmap[name]
def __setitem__(self, name, value):
self.namespace[name] = value
And re-implement __build_class__
such that it wraps the namespace with the wrapper above for the execution of the class body. The original namespace instance prepared by the metaclass is then passed to the metaclass to build the actual class, with the new process of namespace buildup completely transparent to the metaclass.
A reference implementation with no error handling for brevity:
import builtins
from types import resolve_bases, _calculate_meta
def build_class(func, name, *bases, metaclass=type, **kwargs):
metaclass = _calculate_meta(metaclass, bases)
namespace = metaclass.__prepare__(name, bases, **kwargs)
if (resolved_bases := resolve_bases(bases)) is not bases:
namespace['__orig_bases__'] = bases
exec(func.__code__, globals(), InheritedNamespace(resolved_bases, namespace))
return metaclass(name, resolved_bases, namespace, **kwargs)
builtins.__build_class__ = build_class
so that:
class Base:
foo = 'bar'
class Child(Base):
bar = foo
print(Child.bar) # outputs bar
Demo here
Backward Compatibility
Unlike Proposal #1, this proposal completely eliminates the need for library maintainers to refactor their custom namespaces to account for a new built-in custom namespace.
However, existing code with the following (presumably unlikely) pattern where the same name is used both in the base class and outside of it may break and would have to be refactored, or the types
module can offer an implementation of __build_class__
that behaves in the old way to allow easy override and to ease the transition:
foo = 'baz'
class Base:
foo = 'bar'
class Child(Base):
bar = foo # bar is currently 'baz', and would become 'bar' with the proposal
Performance Impact
There will be a miniscule performance impact to name lookups during the execution of a class body due to the namespace wrapper’s delegation, but again, the impact should be minimal.