Background
In current Python, special methods do not behave like normal user-defined methods: they bypass __getattribute__, and they also don’t fall back to __getattr__.
The expression obj[key] only works if the type defines a mapping or sequence slot.
If it does not, Python immediately raises TypeError, even if the object does provide a __getitem__ method dynamically.
This leads to surprising limitations for objects that delegate or map behavior to another object.
Example 1
Suppose we have an object acting as a dynamic view or proxy:
class ConfigView:
def __init__(self, target):
self.target = target
def __getattr__(self, name):
# forward lookups to the underlying object
return getattr(self.target, name)
If the underlying object implements __getitem__, we would naturally expect view["path"] to work the same as view.target["path"]
But currently it fails with:
TypeError: 'ConfigView' object is not subscriptable
even though __getitem__ is available via attribute lookup.
Example 2
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)
It would be better if code below can work:
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)
This example reloads the file at the root level on every access, which would obviously be inefficient in real use. It’s only meant as a demonstration: there are real scenarios where a wrapper needs to delegate almost all operations to the underlying object.
Proposal 1
When a type does not provide a mapping or sequence slot, allow obj[key] to fall back to an attribute lookup for __getitem__, following normal attribute resolution rules (__getattribute__, __getattr__, delegation, etc.).
What’s more, 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.
This would significantly improve the behavior of dynamic proxy objects.
I have made a simple test on __getitem__ in PR to show this idea could work and do no affect to current existing code, and Github Actions show that all tests have been passed(except skipped).
I would like to hear thoughts on whether this fallback would be acceptable as an enhancement.
Additional Question
Inject special method on instance results into unexpected outputs:
import yaml
__all__ = ["config"]
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]
else:
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)
def __setitem__(self, key, value):
raise NotImplementedError(
f"Config object is read-only. Tried to set {key} = {value}"
)
def __setattr__(self, key, value):
raise NotImplementedError(
f"Config object is read-only. Tried to set {key} = {value}"
)
def __delitem__(self, key):
raise NotImplementedError(f"Config object is read-only. Tried to del {key}")
def __delattr__(self, key):
raise NotImplementedError(f"Config object is read-only. Tried to del {key}")
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(_Config):
def __init__(self, filename):
def wrapper(self, func):
def inner(*args, **kwargs):
print("wrapper operated")
self = load_config(filename)
return func(self, *args, **kwargs)
return inner
with open(filename, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
super().__init__(data)
for key, value in _Config.__dict__.items():
if callable(value):
self.__dict__[key] = wrapper(self, value)
config = load_config("config.yaml")
if __name__ == "__main__":
print("show type")
print(config.__getitem__)
print(config.__getattr__)
print(config.__setitem__)
print(config.__setattr__)
print("\ntest read")
print(config.testA.testB["testC"]) #wrapper operated
print(type(config).__getitem__(config, "testA").testB["testC"]) #wrapper bind on load_config, so no output
print(config["testA"].testB["testC"]) #expect output, but no output due to Cpython code. Does this need improvement?
print("\ntest modify")
try:
config.testA.testB["testC"] = 1
except NotImplementedError as e:
print("success:", e)
try:
type(config).__getitem__(config, "testA").testB["testC"] = 1
except NotImplementedError as e:
print("success:", e)
try:
config["testA"].testB["testC"] = 1
except NotImplementedError as e:
print("success:", e)
print("\ntest create")
try:
config.new_attr = 1 #expect output, but no output.
print("faild")
except NotImplementedError as e:
print("success:", e)
try:
config["new_attr"] = 1 #expect output, but no output.
print("faild")
except NotImplementedError as e:
print("success:", e)
output:
show type
<function load_config.__init__.<locals>.wrapper.<locals>.inner at 0x0000000001433CE0>
<function load_config.__init__.<locals>.wrapper.<locals>.inner at 0x0000000001433BA0>
<function load_config.__init__.<locals>.wrapper.<locals>.inner at 0x0000000001433D80>
<function load_config.__init__.<locals>.wrapper.<locals>.inner at 0x0000000001433EC0>
test read
wrapper operated
success
success
success
test modify
wrapper operated
success: Config object is read-only. Tried to set testC = 1
success: Config object is read-only. Tried to set testC = 1
success: Config object is read-only. Tried to set testC = 1
test create
success: Config object is read-only. Tried to set new_attr = 1
success: Config object is read-only. Tried to set new_attr = 1
As we can see how python deal with different special methods on instance is different!
And then inject special methods on class will output expected but different compare to instance:
class load_config:
def wrapper(func):
def inner(self, *args, **kwargs):
print("wrapper operated")
with open(object.__getattribute__(self, "_Config__filename"), "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return func(_Config(data), *args, **kwargs)
return inner
def __init__(self, filename):
object.__setattr__(self, "_Config__filename", filename)
for key, value in _Config.__dict__.items():
if callable(value) and key not in load_config.__dict__.keys():
setattr(load_config, key, load_config.wrapper(value))
config = load_config("config.yaml")
if __name__ == "__main__":
# same test cases as above
output:
show type
<bound method load_config.wrapper.<locals>.inner of <__main__.load_config object at 0x000000000147ECC0>>
<bound method load_config.wrapper.<locals>.inner of <__main__.load_config object at 0x000000000147ECC0>>
<bound method load_config.wrapper.<locals>.inner of <__main__.load_config object at 0x000000000147ECC0>>
<bound method load_config.wrapper.<locals>.inner of <__main__.load_config object at 0x000000000147ECC0>>
test read
wrapper operated
success
wrapper operated
success
wrapper operated
success
test modify
wrapper operated
success: Config object is read-only. Tried to set testC = 1
wrapper operated
success: Config object is read-only. Tried to set testC = 1
wrapper operated
success: Config object is read-only. Tried to set testC = 1
test create
wrapper operated
success: Config object is read-only. Tried to set new_attr = 1
wrapper operated
success: Config object is read-only. Tried to set new_attr = 1
You can see, how python deal with special methods on intance and class is also different!
Proposal 2
Different behavior may brings great trouble to programmers and it is necessary to make a PEP if we want to standardize the behavior of special methods as big change will be employed to python core.
I would like to hear thoughts on whether it’s a good idea to standardize the behavior of special methods.
Remind
This topic has been edited sereral times to show my idea clearly.
I am not English native speaker, some words my be generated by LLM. But, I will try my best to type words by myself now! Some words may cause confusions.
If something I did wrong, just tell me directly!