'Python: Use nested dict to create class of nested dataclass

In the basic case, one can easily map a dictionary to the parameters. The below shows the basic example.

def func1(x: int, y: int):
    return x+y

input = {
    "x": 1,
    "y": 2,
}

## This Works
sum = func1(**input)
# sum = 3

Does Python provide any other types of shortcuts which would enable this type of behavior for nested classes?

from dataclasses import dataclass

@dataclass
class X:
  x: int


@dataclass
class Y:
  y: int


def func2(x: X, y: Y):
    return x.x + y.y


input_2 = {
    "X": {
        "x": 1,
    },
    "Y": {
        "y": 1,
    },
}

sum = func2(**input_2)
# TypeError: func2() got an unexpected keyword argument 'X'

I have tried other approach's. This is an example fo something that works, but is not very general.

sum = func2(X(input_2[X][x]),Y(input_2[Y][y])

Also failed with pydantic

from pydantic import BaseModel

class X(BaseModel):
  x: int


class Y(BaseModel):
  y: int


def func2(x: X, y: Y):
    return x.x + y.y


input_2 = {
    "X": {
        "x": 1,
    },
    "Y": {
        "y": 1,
    },
}
sum = func2(**input_2)


Solution 1:[1]

I think create a new class that includes X and Y, assume C can work for your case

from pydantic import BaseModel

class X(BaseModel):
  x: int


class Y(BaseModel):
  y: int


class C(X, Y):
    pass

def func2(c: C):
    x = c.x
    y = c.y
    return x + y


input_2 = C(**{
    "x": 1,
    "y": 1,
})
sum = func2(input_2)
print(sum)

Solution 2:[2]

You can use a decorator to convert each dict argument for a function parameter to its annotated type, assuming the type is a dataclass or a BaseModel in this case.

An example with the dataclass-wizard - which should also support a nested dataclass model:

import functools

from dataclasses import dataclass, is_dataclass
from dataclass_wizard import fromdict


def transform_dict_to_obj(f):
    name_to_tp = {name: tp for name, tp in f.__annotations__.items()
                  if is_dataclass(tp)}

    @functools.wraps(f)
    def new_func(**kwargs):
        for name, tp in name_to_tp.items():
            if name in kwargs:
                kwargs[name] = fromdict(tp, kwargs[name])

        return f(**kwargs)

    return new_func


@dataclass
class X:
  x: int


@dataclass
class Y:
  y: int


@transform_dict_to_obj
def func2(*, x: X, y: Y) -> str:
    return x.x + y.y


input_2 = {
    "x": {
        "x": 1,
    },
    "y": {
        "y": 1,
    },
}

sum = func2(**input_2)

print('Sum:', sum)
assert sum == 2  # OK

Similarly, with pydantic:

import functools

from pydantic import BaseModel

class X(BaseModel):
  x: int


class Y(BaseModel):
  y: int


def transform_dict_to_obj(f):
    name_to_from_dict = {name: tp.parse_obj
                         for name, tp in f.__annotations__.items()
                         if issubclass(tp, BaseModel)}

    @functools.wraps(f)
    def new_func(**kwargs):
        for name, from_dict in name_to_from_dict.items():
            if name in kwargs:
                kwargs[name] = from_dict(kwargs[name])

        return f(**kwargs)

    return new_func


@transform_dict_to_obj
def func2(*, x: X, y: Y) -> str:
    return x.x + y.y


input_2 = {
    "x": {
        "x": 1,
    },
    "y": {
        "y": 1,
    },
}

sum = func2(**input_2)

print('Sum:', sum)
assert sum == 2  # OK

For a slightly more optimized version, instead of using a for loop in the decorator each time, you can only add the logic you need to run, and then generate the new function using dataclasses._create_fn() or similar:

from dataclasses import dataclass, is_dataclass, _create_fn
from dataclass_wizard import fromdict


def transform_dict_to_obj_optimized(f):
    args = []
    body_lines = []
    return_type = f.__annotations__.pop('return', None)

    for name, tp in f.__annotations__.items():
        type_name = tp.__qualname__
        args.append(name)

        if is_dataclass(tp):
            body_lines.append(f'if {name}:')
            body_lines.append(f' {name} = fromdict({type_name}, {name})')

    body_lines.append(f'return original_fn({",".join(args)})')

    return _create_fn(f.__name__, args, body_lines,
                      return_type=return_type,
                      locals={'original_fn': f},
                      globals=globals())


@dataclass
class X:
  x: int


@dataclass
class Y:
  y: int


@transform_dict_to_obj_optimized
def func2(x: X, y: Y) -> int:
    return x.x + y.y


input_2 = {
    "x": {
        "x": 1,
    },
    "y": {
        "y": 1,
    },
}

sum = func2(**input_2)

print('Sum:', sum)
assert sum == 2  # OK

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Tony
Solution 2