Bug description:
I have a custom Mapping type. I am unpacking it with eg {**mymapping}. If, in either the keys() or the the __getitem__() method, I raise most kinds of errors, such as a ValueError, these are reported correctly. BUT, if I raise an AttributeError, then this error isn't reported properly, instead I get TypeError: 'MyMapping' object is not a mapping, masking the actual error:
class MyMapping:
def __init__(
self,
*,
raises_on_keys: type[Exception] | None = None,
raises_on_getitem: type[Exception] | None = None,
):
self.raises_on_keys = raises_on_keys
self.raises_on_getitem = raises_on_getitem
def __getitem__(self, key):
if self.raises_on_getitem:
raise self.raises_on_getitem("error in __getitem__")
return key * 2
def keys(self):
if self.raises_on_keys:
raise self.raises_on_keys("error in keys")
return [1, 2, 3]
options = [
None,
ValueError,
AttributeError,
]
outcomes = []
for raises_on_keys in options:
for raises_on_getitem in options:
try:
d = {
**MyMapping(
raises_on_keys=raises_on_keys, raises_on_getitem=raises_on_getitem
)
}
outcomes.append((raises_on_keys, raises_on_getitem, "Success", d))
except Exception as e:
outcomes.append((raises_on_keys, raises_on_getitem, "Exception", str(e)))
# format to markdown table
print("| raises_on_keys | raises_on_getitem | outcome | result |")
print("| --- | --- | --- | --- |")
for raises_on_keys, raises_on_getitem, outcome, result in outcomes:
raises_on_keys_str = raises_on_keys.__name__ if raises_on_keys else "None"
raises_on_getitem_str = raises_on_getitem.__name__ if raises_on_getitem else "None"
print(
f"| {raises_on_keys_str} | {raises_on_getitem_str} | {outcome} | `{result}` |"
)
Ran with uv run --python 3.14 bug.py, which resolves to python 3.14.2. This gives:
| raises_on_keys |
raises_on_getitem |
error |
| None |
None |
`` |
| None |
ValueError |
ValueError: error in __getitem__ |
| None |
AttributeError |
TypeError: 'MyMapping' object is not a mapping |
| ValueError |
None |
ValueError: error in keys |
| ValueError |
ValueError |
ValueError: error in keys |
| ValueError |
AttributeError |
ValueError: error in keys |
| AttributeError |
None |
TypeError: 'MyMapping' object is not a mapping |
| AttributeError |
ValueError |
TypeError: 'MyMapping' object is not a mapping |
| AttributeError |
AttributeError |
TypeError: 'MyMapping' object is not a mapping |
What I would expect is for all of the TypeError: 'MyMapping' object is not a mapping errors to actually be AttributeError: error in keys or AttributeError: error in __getitem__ errors.
I assume this is because in the implementation, it does assumes ducktyping, and the raised attribute error is interpreted as "the passed object doesn't even have a keys()/__getitem__ method"
eg guessing this is how this is currently implemented:
try:
for key in obj.keys():
yield key, obj.__getitem__(key)
except AttributeError as e:
raise TypeError(f"'{type(obj).__name__}' object is not a mapping")
What I think SHOULD happen:
try:
keys = obj.keys
except AttributeError as e:
raise TypeError(f"'{type(obj).__name__}' object is not a mapping")
for key in keys():
try:
getter = obj.__getitem__
except AttributeError as e:
raise TypeError(f"'{type(obj).__name__}' object is not a mapping")
yield key, getter(key)
EDIT: Actually this should be more performant, only 2 checks, instead of N checks, one per key. (Also, for the record, this includes suggestion to improve the error messages, but that should definitely be a separate PR)
try:
keys = obj.keys
except AttributeError as e:
raise TypeError(f"'{type(obj).__name__}' object requires a .keys() method to be used as a mapping")
try:
getter = obj.__getitem__
except AttributeError as e:
raise TypeError(f"'{type(obj).__name__}' object requires a .__getitem__() method to be used as a mapping")
for key in keys():
yield key, getter(key)
CPython versions tested on:
3.14
Operating systems tested on:
macOS
Linked PRs
Bug description:
I have a custom Mapping type. I am unpacking it with eg
{**mymapping}. If, in either thekeys()or the the__getitem__()method, I raise most kinds of errors, such as a ValueError, these are reported correctly. BUT, if I raise an AttributeError, then this error isn't reported properly, instead I getTypeError: 'MyMapping' object is not a mapping, masking the actual error:Ran with
uv run --python 3.14 bug.py, which resolves to python3.14.2. This gives:ValueError: error in __getitem__TypeError: 'MyMapping' object is not a mappingValueError: error in keysValueError: error in keysValueError: error in keysTypeError: 'MyMapping' object is not a mappingTypeError: 'MyMapping' object is not a mappingTypeError: 'MyMapping' object is not a mappingWhat I would expect is for all of the
TypeError: 'MyMapping' object is not a mappingerrors to actually beAttributeError: error in keysorAttributeError: error in __getitem__errors.I assume this is because in the implementation, it does assumes ducktyping, and the raised attribute error is interpreted as "the passed object doesn't even have a
keys()/__getitem__method"eg guessing this is how this is currently implemented:
What I think SHOULD happen:
EDIT: Actually this should be more performant, only 2 checks, instead of N checks, one per key. (Also, for the record, this includes suggestion to improve the error messages, but that should definitely be a separate PR)
CPython versions tested on:
3.14
Operating systems tested on:
macOS
Linked PRs