Dictionaries as Records

So far, we have seen how dictionaries can be used as a basic mutable data structure with keys and values. Dictionaries can also be used as records, where each key represents a field in a record, and the corresponding value represents the value of that field. This is similar to how dataclasses work in Python, where each attribute represents a field in a data structure.

Mixed Types

person = {
    'name': 'Ada',
    'age': 33,
    'city': 'Newark'
}

print(person)
# Output: {'name': 'Ada', 'age': 33, 'city': 'Newark'}

One of the advantages of using dictionaries as records is that the values do not have to be of the same type. In the example shown here, we define a dictionary representing a Person record with three fields: name, age, and city. The name field is a string, the age field is an integer, and the city field is a string. This flexibility allows us to represent complex data structures using dictionaries.

Unfortunately, this does make it harder to enforce a consistent structure across multiple records. Dataclasses provide a way to define a consistent structure for data records. The advantage of using dictionaries is that we can easily create and manipulate records without the need to define a class, and we can add or remove fields as needed.

Type of Records

person = {
    'name': 'Ada',
    'age': 33,
    'city': 'Newark'
}

def print_person(person: dict):
    print(f"Name: {person['name']}")
    print(f"Age: {person['age']}")
    print(f"City: {person['city']}")

print_person(person)

What type of dictionary is being passed into the print_person function? We cannot simply say dict[str, str], because one of the fields is an integer. A more accurate type would be dict[str, str | int], which indicates that the values can be either strings or integers. However, when iterating over a key and value pair, the type of the value is either a string or an integer, not both, and we have to be careful what operations we would work with. There are sophisticated ways of handling this, but for now, we will keep it simple and avoid mixing our types in a single dictionary. The only exception will be to convert a dictionary to a dataclass, and vice versa.

The asdict function

from dataclasses import dataclass, asdict

@dataclass
class Person:
    name: str
    age: int
    city: str

person = Person("Ada", 33, "Newark")

person_dict = asdict(person)

print(person_dict)
# Output: {'name': 'Ada', 'age': 33, 'city': 'Newark'}

Since dataclasses and dictionaries share a similar structure, we can convert a dataclass instance to a dictionary. The dataclasses module provides an asdict function. This function converts the dataclass instance to a dictionary where the keys are the field names and the values are the corresponding values of the instance.

In the example shown here, we define a Person dataclass with three fields: name, age, and city. We then create an instance of the Person class with the values "Ada", 33, and "Newark". The resulting dictionary contains the field names as keys and the corresponding values as values.

Nested Instances

from dataclasses import dataclass, asdict

@dataclass
class Address:
    street: str
    city: str
    zip_code: str

@dataclass
class Person:
    name: str
    age: int
    address: Address

address = Address("123 Main St", "Newark", "19711")
person = Person("Ada", 33, address)

person_dict = asdict(person)
print(person_dict)

The asdict function can handle nested dataclasses as well. In the example shown here, we define two dataclasses: Address and Person. The Person dataclass has a field address that is an instance of the Address dataclass. When we convert the Person instance to a dictionary, the resulting dictionary will contain the nested dataclass instance as another dictionary. The asdict function handles the conversion of nested dataclasses to dictionaries recursively, ensuring that the resulting dictionary contains all the nested data as dictionaries. The function also works for dictionaries that contain other dictionaries, lists, or other complex data structures.

Dictionaries to Dataclasses

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int
    city: str

person_dict = {'name': 'Ada', 'age': 33, 'city': 'Newark'}

person = Person(**person_dict)
print(person)
# Output: Person(name='Ada', age=33, city='Newark')

We can also convert dictionaries to dataclasses using dictionary unpacking. In the example shown here, we define a Person dataclass with three fields: name, age, and city. We then create a dictionary with the field names as keys and the corresponding values as values. By using the ** operator to unpack the dictionary into the dataclass constructor, we can create an instance of the Person class with the values from the dictionary.

Nested Dictionaries

from dataclasses import dataclass

@dataclass
class Address:
    street: str
    city: str
    zip_code: str

@dataclass
class Person:
    name: str
    age: int
    address: Address

person_dict = {
    'name': 'Ada',
    'age': 33,
    'address': {
        'street': '123 Main St',
        'city': 'Newark',
        'zip_code': '19711'
    }
}

person = Person(**person_dict)
print(person)

print(type(person.address))
# Output: <class 'dict'>

Unfortunately, when using dictionary unpacking to create nested dataclasses, the nested dictionaries are not automatically converted to dataclass instances. In the example shown here, we define two dataclasses: Address and Person. We create a dictionary with the field names as keys and the corresponding values as values. The address field in the dictionary is another dictionary representing the Address dataclass. When we unpack the dictionary into the Person constructor, the address field remains a dictionary, not an instance of the Address dataclass.

In order to convert nested dictionaries to nested dataclass instances, we need to manually convert the nested dictionaries to dataclass instances before creating the parent dataclass instance.

Correctly Handling Nested Dictionaries

from dataclasses import dataclass

@dataclass
class Address:
    street: str
    city: str
    zip_code: str

@dataclass
class Person:
    name: str
    age: int
    address: Address

def dict_to_person(person_dict: dict) -> Person:
    return Person(
        name=person_dict['name'],
        age=person_dict['age'],
        address=Address(**person_dict['address'])
    )

person_dict = {
    'name': 'Ada',
    'age': 33,
    'address': {
        'street': '123 Main St',
        'city': 'Newark',
        'zip_code': '19711'
    }
}
print(dict_to_person(person_dict))

To correctly handle nested dictionaries and convert them to nested dataclass instances, we can define a function that takes a dictionary and returns an instance of the parent dataclass with the nested dataclass instances correctly converted. In the example shown here, we define a dict_to_person function that takes a dictionary representing a Person instance and returns a Person instance with the address field converted to an instance of the Address dataclass.

By manually converting the nested dictionaries to nested dataclass instances, we can ensure that the resulting dataclass instances contain the correct data and maintain the structure of the dataclasses.

Summary