Let me clarify my proposal a bit, because my earlier explanation may not have been clear enough.
What I noticed is that, in CPython, special methods do not behave like normal user-defined methods: they bypass __getattribute__, and they also don’t fall back to __getattr__. Ideally, one might imagine a model where special methods participate in the same lookup chain as regular methods — but introducing that globally, for all special methods, would be a far-reaching change, and even if it were technically possible, it would absolutely require a PEP.
That’s why my patch does not change the general special-method model.
Instead, it adds a fallback after the existing slot-based lookup fails, and only for __getitem__. In this approach:
- the normal behavior stays untouched,
- the fallback runs only when the special method is not defined,
- and the patch does not affect any existing code paths.
As you mentioned, this idea could theoretically be extended to other special methods. I haven’t done that because I simply don’t have the bandwidth to evaluate every method thoroughly — so I only implemented and tested __getitem__ for now. But the same pattern could apply to methods like __contains__ or __len__, and even __eq__ wouldn’t be impacted, because Python automatically provides an implementation when one is not defined, meaning the fallback would never trigger for it anyway.
So this proposal isn’t about making __getitem__ behave differently from other special methods — it’s about adding a narrow, post-failure fallback that preserves all existing semantics, while making delegation-based patterns slightly less repetitive.
Here is a concrete example:
1. Original version (without any fallback patch)
When using _Config as a read-only, delegation-based wrapper, a load_config proxy must manually forward every operation it wants to expose:
class _Config(dict):
def __getitem__(self, key):
value = super().__getitem__(key)
if isinstance(value, dict):
return _Config(value)
elif isinstance(value, list):
return [_Config(v) if isinstance(v, dict) else v for v in value]
return value
def __getattr__(self, key):
if key.startswith("__") and key.endswith("__"):
cls = type(self)
if key in cls.__dict__:
func = cls.__dict__[key]
return func.__get__(self, cls)
return super().__getattribute__(key)
return self.__getitem__(key)
# Explicitly block all mutation to preserve immutability
def __setitem__(self, key, value): raise NotImplementedError
def __setattr__(self, key, value): raise NotImplementedError
def __delitem__(self, key): raise NotImplementedError
def __delattr__(self, key): raise NotImplementedError
def items(self):
for key, value in super().items():
if isinstance(value, dict):
yield key, _Config(value)
elif isinstance(value, list):
yield key, [_Config(v) if isinstance(v, dict) else v for v in value]
else:
yield key, value
class load_config:
def __init__(self, filepath):
self.filepath = filepath
def _load(self):
row = yaml.safe_load(open(self.filepath, "r", encoding="utf-8"))
return _Config(row)
# Without a fallback, every operation must be forwarded manually
def __getitem__(self, key): return self._load()[key]
def __getattr__(self, name): return getattr(self._load(), name)
def __setitem__(self, key, value): return self._load().__setitem__(key, value)
def __setattr__(self, key, value):
if key == "filepath":
return super().__setattr__(key, value)
return setattr(self._load(), key, value)
def __delitem__(self, key): return self._load().__delitem__(key)
def __delattr__(self, key):
if key == "filepath":
return super().__delattr__(key)
return delattr(self._load(), key)
This is a large amount of boilerplate for something that is conceptually just:
“load the file and then behave exactly like _Config.”
2. Version with the proposed fallback patch
With a fallback that applies only when the class has no corresponding slot or method, the proxy can be dramatically simplified:
class load_config:
def __init__(self, filepath: str):
self.filepath = filepath
def __getattribute__(self, key):
row = yaml.safe_load(open(
object.__getattribute__(self, "filepath"),
"r",
encoding="utf-8"
).read())
return _Config(row).__getattr__(key)
That’s all that is needed.
Because _Config already implements (and blocks) all relevant special methods, the proxy no longer has to forward: __getitem__, __setitem__, __delitem__, __setattr__, __delattr__ and so on.
The fallback means the proxy “inherits” all these behaviors automatically — only in the case where it has not defined them itself — which is exactly the desired delegation pattern.
And since the fallback is triggered only after normal slot lookup fails, there is no conflict when key = filepath.