Search code examples
pydanticpython-dataclassespython-3.11

Unable to call .dict() method on custom Pydantic models (exception: TypeError: unhashable type: 'dict')


I am new to dataclasses/Pydantic models and I am running into an issue elaborated below. I have defined two pydantic models Roles and SubRoles such that Role model contains a set of SubRoles.

"""
Module contains all the models.
"""

# pylint: disable=too-few-public-methods


import typing
import pydantic


class RoleBaseClass(pydantic.BaseModel):  # pylint: disable=no-member
    """
    Base class for all the models.
    """
    name: str = pydantic.Field(regex=r"^\w+$")

    def __hash__(self: typing.Self) -> int:
        return hash(self.name)

    def __eq__(self, other) -> bool:
        return self.name == other.name


class SubRole(RoleBaseClass):
    """
    SubRole model.
    """

class Role(RoleBaseClass):
    """
    Role model.
    """
    subroles: set[SubRole] = pydantic.Field(default=set())

    def __str__(self) -> str:
        base_str: str = super().__str__()
        cls_name: str = self.__class__.__name__
        return f"{cls_name}({base_str})"


if __name__ == "__main__":

    data0 = Role(
        name="aws0",
    )

    data1 = Role(
        name="aws1",
        subroles=set(
            [
                SubRole(name="aws1sub1"),
                SubRole(name="aws1sub2"),
            ]
        ),
    )

    data2 = Role(
        name="aws1",
        subroles=set(
            [
                SubRole(name="aws2sub1"),
                SubRole(name="aws2sub2"),
            ]
        ),
    )

    print(data0)
    print(data1.subroles)
    print(data1 == data2)
    
    # Below line fails with TypeError: unhashable type: 'dict'
    print(data2.dict())

everything works apart from calling Role().dict() method which fails with TypeError: unhashable type: 'dict' error, traceback below

Traceback (most recent call last):
  File "models.py", line 72, in <module>
    print(data2.dict())
          ^^^^^^^^^^^^
  File "pydantic\main.py", line 449, in pydantic.main.BaseModel.dict
  File "pydantic\main.py", line 868, in _iter
  File "pydantic\main.py", line 794, in pydantic.main.BaseModel._get_value
TypeError: unhashable type: 'dict'

Could anyone suggest the mistake I made, also any further improvement to the code.

Cheers, DD.


Solution

  • In your case, the set() object is not hashable. Here are 3 solutions of various elegance:

    1. A quick dirty fix is to use a list instead:

    You can enforce deduplication via a validator if needed.

        subroles: list[SubRole]
    

    2. Another quick fix is to use the argument models_as_dict=False when serializing to json:

    Note: this will only work to json, see next solution for serializing to a dict...

    print(data2.json(models_as_dict=False))
    

    3. To serialize your set to a dict(), use the self._iter() method, as follows:

    Note: self._iter() is wrapped by the self.dict() method, so you are basically accessing the underlying functionality directly.

    my_dict = dict(data2._iter(to_dict=False))
    print(my_dict)
    

    A proper solution: Override __getitem__()

    This goes a bit beyond my scope of understanding, but this github conversation thread would be a good place to start: https://github.com/pydantic/pydantic/issues/380#issuecomment-459352718. (Note, this would probably be the best solution for you.)

    Extra Stuff:

    1. You should be using default_factory=set instead, since default=list|set will create problems for you because of mutable default arguments:
    class Role(RoleBaseClass):
        """
        Role model.
        """
        subroles: list[SubRole] = pydantic.Field(default_factory=list)
    
    1. Presently, Role.json(models_as_dict=False) will give you a list of dictionary objects.
    {"name": "aws1", "subroles": [{"name": "aws2sub2"}, {"name": "aws2sub1"}]}
    

    If you want a list of attributes, use a custom json_encoder like so:

    class Role(RoleBaseClass):
        """
        Role model.
        """
        subroles: typing.Set[SubRole] = pydantic.Field(default_factory=set)
    
        class Config:
            json_encoders = {
                SubRole: lambda x: x.name,
            }
    

    This will give you a list of names:

    {"name": "aws1", "subroles": ["aws2sub1", "aws2sub2"]}