Class that acts as mapping for **unpacking
Question:
Without subclassing dict, what would a class need to be considered a mapping so that it can be passed to a method with **
.
from abc import ABCMeta
class uobj:
__metaclass__ = ABCMeta
uobj.register(dict)
def f(**k): return k
o = uobj()
f(**o)
# outputs: f() argument after ** must be a mapping, not uobj
At least to the point where it throws errors of missing functionality of mapping, so I can begin implementing.
I reviewed emulating container types but simply defining magic methods has no effect, and using ABCMeta
to override and register it as a dict validates assertions as subclass, but fails isinstance(o, dict)
. Ideally, I dont even want to use ABCMeta
.
Answers:
The __getitem__()
and keys()
methods will suffice:
>>> class D:
def keys(self):
return ['a', 'b']
def __getitem__(self, key):
return key.upper()
>>> def f(**kwds):
print kwds
>>> f(**D())
{'a': 'A', 'b': 'B'}
If you’re trying to create a Mapping — not just satisfy the requirements for passing to a function — then you really should inherit from collections.abc.Mapping
. As described in the documentation, you need to implement just:
__getitem__
__len__
__iter__
The Mixin will implement everything else for you: __contains__
, keys
, items
, values
, get
, __eq__
, and __ne__
.
The answer can be found by digging through the source.
When attempting to use a non-mapping object with **
, the following error is given:
TypeError: 'Foo' object is not a mapping
If we search CPython’s source for that error, we can find the code that causes that error to be raised:
case TARGET(DICT_UPDATE): {
PyObject *update = POP();
PyObject *dict = PEEK(oparg);
if (PyDict_Update(dict, update) < 0) {
if (_PyErr_ExceptionMatches(tstate, PyExc_AttributeError)) {
_PyErr_Format(tstate, PyExc_TypeError,
"'%.200s' object is not a mapping",
Py_TYPE(update)->tp_name);
PyDict_Update
is actually dict_merge
, and the error is thrown when dict_merge
returns a negative number. If we check the source for dict_merge
, we can see what leads to -1 being returned:
/* We accept for the argument either a concrete dictionary object,
* or an abstract "mapping" object. For the former, we can do
* things quite efficiently. For the latter, we only require that
* PyMapping_Keys() and PyObject_GetItem() be supported.
*/
if (a == NULL || !PyDict_Check(a) || b == NULL) {
PyErr_BadInternalCall();
return -1;
The key part being:
For the latter, we only require that PyMapping_Keys() and PyObject_GetItem() be supported.
Using dataclasses
Cleaner and ultimatelly turns out to be better in terms of quality, the usage of dataclass, which helps also return the correct object to keys
method.
from dataclasses import dataclass
@dataclass(frozen=True)
class Person:
name: str
surname: str
age: int
def __getitem__(self, key):
return getattr(self, key)
def keys(self):
return self.__annotations__.keys()
josh_doe: Person = Person("John", "Doe", 31)
print(f"John object : {josh_doe}")
user_data = {**josh_doe}
print(user_data)
Without subclassing dict, what would a class need to be considered a mapping so that it can be passed to a method with **
.
from abc import ABCMeta
class uobj:
__metaclass__ = ABCMeta
uobj.register(dict)
def f(**k): return k
o = uobj()
f(**o)
# outputs: f() argument after ** must be a mapping, not uobj
At least to the point where it throws errors of missing functionality of mapping, so I can begin implementing.
I reviewed emulating container types but simply defining magic methods has no effect, and using ABCMeta
to override and register it as a dict validates assertions as subclass, but fails isinstance(o, dict)
. Ideally, I dont even want to use ABCMeta
.
The __getitem__()
and keys()
methods will suffice:
>>> class D:
def keys(self):
return ['a', 'b']
def __getitem__(self, key):
return key.upper()
>>> def f(**kwds):
print kwds
>>> f(**D())
{'a': 'A', 'b': 'B'}
If you’re trying to create a Mapping — not just satisfy the requirements for passing to a function — then you really should inherit from collections.abc.Mapping
. As described in the documentation, you need to implement just:
__getitem__
__len__
__iter__
The Mixin will implement everything else for you: __contains__
, keys
, items
, values
, get
, __eq__
, and __ne__
.
The answer can be found by digging through the source.
When attempting to use a non-mapping object with **
, the following error is given:
TypeError: 'Foo' object is not a mapping
If we search CPython’s source for that error, we can find the code that causes that error to be raised:
case TARGET(DICT_UPDATE): {
PyObject *update = POP();
PyObject *dict = PEEK(oparg);
if (PyDict_Update(dict, update) < 0) {
if (_PyErr_ExceptionMatches(tstate, PyExc_AttributeError)) {
_PyErr_Format(tstate, PyExc_TypeError,
"'%.200s' object is not a mapping",
Py_TYPE(update)->tp_name);
PyDict_Update
is actually dict_merge
, and the error is thrown when dict_merge
returns a negative number. If we check the source for dict_merge
, we can see what leads to -1 being returned:
/* We accept for the argument either a concrete dictionary object,
* or an abstract "mapping" object. For the former, we can do
* things quite efficiently. For the latter, we only require that
* PyMapping_Keys() and PyObject_GetItem() be supported.
*/
if (a == NULL || !PyDict_Check(a) || b == NULL) {
PyErr_BadInternalCall();
return -1;
The key part being:
For the latter, we only require that PyMapping_Keys() and PyObject_GetItem() be supported.
Using dataclasses
Cleaner and ultimatelly turns out to be better in terms of quality, the usage of dataclass, which helps also return the correct object to keys
method.
from dataclasses import dataclass
@dataclass(frozen=True)
class Person:
name: str
surname: str
age: int
def __getitem__(self, key):
return getattr(self, key)
def keys(self):
return self.__annotations__.keys()
josh_doe: Person = Person("John", "Doe", 31)
print(f"John object : {josh_doe}")
user_data = {**josh_doe}
print(user_data)