Dataclasses.asdict - transformation of dict-fields - change type(obj) to dict directly

Hello all,

I refer to the current implementation of the public method asdict within dataclasses-module transforming the dataclass input to a dictionary.

Sometimes, a dataclass has itself a dictionary as field.
Therefore, the current implementation is used for transformation ( see. cpython/dataclasses.py at 0a7936a38f0bab1619ee9fe257880a51c9d839d5 · python/cpython · GitHub):

...
    elif isinstance(obj, dict):
        return type(obj)((_asdict_inner(k, dict_factory),
                          _asdict_inner(v, dict_factory))
                         for k, v in obj.items())
...

As it is already clear, that obj is a dict, I prefer to change this line to

...
    elif isinstance(obj, dict):
        return dict((_asdict_inner(k, dict_factory),
                          _asdict_inner(v, dict_factory))
                         for k, v in obj.items())
...

Otherwise, using dataclasses in combination with SqlAlchemy, the current implementation of asdict leads to an error, as already reported here:

What do you think?

Thanks and best,
Christoph

It is not clear the object is a dict, this also catches subclasses of dict.

>>> class MyDict(dict): ...
...
>>> isinstance(MyDict(), dict)
True

This change would alter the behaviour of asdict in cases where subclasses of dict being returned as-is is relied upon.

Where asdict_altered has this change.


class MyDict(dict):
    """Assume this does something useful"""
    def __repr__(self):
        original_repr = super().__repr__()
        return f"MyDict({original_repr})"

@dataclass
class X:
    x: MyDict = field(default_factory=MyDict)


inst = X()
inst.x["Key"] = "value"

converted = asdict(inst, dict_factory=MyDict)
new_converted = asdict_altered(inst, dict_factory=MyDict)

print(f"{converted=}")
print(f"{new_converted=}")

Output:

converted=MyDict({'x': MyDict({'Key': 'value'})})
new_converted=MyDict({'x': {'Key': 'value'}})

In this case MyDict doesn’t do anything useful but if it did you’ve potentially broken code that relies on asdict returning the subclass.

Ok check. Thanks for clarification. Make sense.

However, adding an init-Method, the asdict-method leads to the following behavior.

Reproduce the error

from dataclasses import field, asdict, dataclass


class MyDict(dict):

    def __init__(self):
        pass

    """Assume this does something useful"""
    def __repr__(self):
        original_repr = super().__repr__()
        return f"MyDict({original_repr})"

@dataclass
class X:
    x: MyDict = field(default_factory=MyDict)


inst = X()
inst.x["Key"] = "value"

converted = asdict(inst, dict_factory=MyDict)

print(f"{converted=}")

Error

Traceback (most recent call last):
  File "/home/cschroeer/python-projects/dataclasses-asdict-core/mydict.py", line 22, in <module>
    converted = asdict(inst, dict_factory=MyDict)
  File "/usr/local/lib/python3.10/dataclasses.py", line 1238, in asdict
    return _asdict_inner(obj, dict_factory)
  File "/usr/local/lib/python3.10/dataclasses.py", line 1245, in _asdict_inner
    value = _asdict_inner(getattr(obj, f.name), dict_factory)
  File "/usr/local/lib/python3.10/dataclasses.py", line 1275, in _asdict_inner
    return type(obj)((_asdict_inner(k, dict_factory),
TypeError: MyDict.__init__() takes 1 positional argument but 2 were given

Is this also expected?

Well, yes - you’ve changed the __init__ which is what’s being used by type(cls)(...). This usage requires the subclass to have the same signature as dict (or at least a compatible signature). This would also fail to function as the argument to dict_factory even with the change.

1 Like

Thanks for clarification :slight_smile: