7 ways to implement DTOs in Python and what to keep in mind
Tags: python, 5days5blogposts, programming, learning
Published: 2021-04-19 21:54:44

---
title: 7 ways to implement DTOs in Python and what to keep in mind
published: true
description: 
tags: python, 5days5blogposts, programming, learning
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50s2negn7d1i1j0ik14r.gif
---

[Data Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) are simply data structures typically used to pass data between application layers or between services. 

The simplest form of DTO in Python can be just a **dictionary**:

```python
itemdto = {
    "name": "Potion",
    "location": Location("Various"),
    "description": "Recover 20 HP",
}
```
Issues (among others): mutable, lack of typing, lack of specificity.

*We will use here the `Location` class, which might be a model class managed by an ORM in the real-life scenario:*

```python
class Location:
    def __init__(self, name: str) -> None:
        self.name = name

    def __repr__(self) -> str:
        return f"Location(name={self.name})"

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

If we want to set the attributes of a DTO more precisely, we can define a separate **class**:

```python
class ItemDto:
    def __init__(self, name: str, location: Location, description: str = "") -> None:
        self.name = name
        self.location = location
        self.description = description
```

Alternatively, we can use `kwargs` and a `.get()` method for optional parameters:

```python
class ItemDto:
    def __init__(self, **kwargs) -> None:
        self.name = kwargs["name"]
        self.location = kwargs["location"]
        self.description = kwargs.get("description", "")

itemdto = ItemDto(
    name="Super Potion",
    location=Location("Various"),
    description="Recover 70 HP"
)
```

Well defined DTOs can give us more benefits, such as making it easier to perform serialization or validation. Here are a few examples of using different features of Python standard library and 3rd party packages to create better DTOs.

## stdlib solutions

### dataclasses

* added to Python 3.7 (and later backported to Python 3.6)

* created using a `@dataclass` decorator

* by default add automatically generated dunder methods `__init__`, `__repr__` and `__eq__`

* `__init__` method takes all fields as method parameters and sets their values to instance attributes with the same names:

```python
from dataclasses import dataclass

@dataclass
class ItemDto:
    name: str
    location: Location
    description: str = ""


# support both positional and keyword args
itemdto = ItemDto(
    name="Old Rod",
    location=Location("Vermillion City"),
    description="Fish for low-level Pokemon",
)
```

* generated `__repr__` method returns a string containing class name, field names and field representation

```python
>>> print(itemdto)
ItemDto(name='Old Rod', location=Location(name=Vermillion City), description='Fish for low-level Pokemon')
```

* generated `__eq__` method compares the class tuples containing field values of the current and the other instance

```python
itemdto2 = ItemDto(
    name="Old Rod",
    location=Location("Vermillion City"),
    description="Fish for low-level Pokemon",
)

>>> itemdto == itemdto2
True
```

* `__eq__` method works the same as if we would explicitly declare it this way:

```python
def __eq__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.location, self.description) == (other.name, other.location, other.description)
    return NotImplemented
```

* it might be a good idea to make DTO instances immutable. It is possible by setting the argument `frozen` to `True`:

```python
@dataclass(frozen=True)
class ItemDto:
    name: str
    location: Location
    description: str = ""
```

* not iterable:

```python
...: for field in itemdto:
...:     print(field)
...: 
TypeError: 'ItemDto' object is not iterable
```

More on dataclasses:
* https://docs.python.org/3/library/dataclasses.html#module-dataclasses 
* https://realpython.com/python-data-classes/


### NamedTuples

* `NamedTuple` is a subclass of regular tuple
* introduced in Python 3.0 as a factory method in the collections module:

```python
from collections import namedtuple

ItemDto = namedtuple("ItemDto", ["name", "location", "description"])
```

* added to Python 3.5 as a typed version in typed module and later enhanced with variable annotations syntax in Python 3.6:

```python
from typing import NamedTuple

class ItemDto(NamedTuple):
    name: str
    location: Location
    description: str = ""


# support both positional and keyword args
itemdto = ItemDto(
    "X Speed", "Temporarily raise Speed in battle", Location("Celadon Dept. Store")
)

>>> print(itemdto)
ItemDto(name='X Speed', location='Temporarily raise Speed in battle', description='Celadon Dept. Store')
```

* immutable
* `__repr__` and `__eq__` handled
* iterable

```python
...: for field in itemdto:
...:     print(field)
...: 
X Speed
Temporarily raise Speed in battle
Location(name=Celadon Dept. Store)
```

* support default values, although these must be defined after any fields without default values.

More on NamedTuples:
* https://docs.python.org/3/library/typing.html#typing.NamedTuple

### TypedDicts

* available since Python 3.8:

```python
from typing import TypedDict

class ItemDto(TypedDict):
    name: str
    location: Location
    description: str


itemdto = ItemDto(
    name="Escape Rope,",
    location=Location("Various"),
    description="Teleport to last visited Pokemon Center",
)

>>> print(itemdto)
{'name': 'Escape Rope,', 'location': Location(name=Various), 'description': 'Teleport to last visited Pokemon Center'}
```

* mutable
* `__repr__` and `__eq__` handled
* iterable in *dict* kind of way
* don't support default values
* can provide typing for existing dictionaries
* since those are still dictionaries, after all, they can be directly serialized to JSON data structures *(although in this example, we should provide a custom encoder for the `Location` class).*

More on TypedDicts:
* https://www.python.org/dev/peps/pep-0589/


## 3rd party packages

### attrs

* a pytest dependency, so there's a chance you might already have it in your project
* similar to the dataclasses, in fact, the attrs library was the basis for designing the dataclasses:

```python
import attr

@attr.s(frozen=True)
class ItemDto:
    name: str = attr.ib()
    location: Location = attr.ib()
    description: str = attr.ib(default="")

# also, the dataclasses syntax!
@attr.dataclass(frozen=True)
class ItemDto:
    name: str
    location: Location
    description: str = ""
```

* attrs provide some extra functionality on top of those that the dataclasses offer, like runtime validation and memory optimization (slotted classes).


Here's an example of runtime validation:

```python
@attr.s(frozen=True)
class PokemonDto:
    name: str = attr.ib()
    type: str = attr.ib(
        validator=attr.validators.in_(
            [
                "Fire",
                "Water",
                "Electric",
                "Poison",  # ...
            ]
        )
    )

>>> PokemonDto("Charmander", "Fire")
PokemonDto(name='Charmander', type='Fire')

>>> PokemonDto("Charmander", "Gyarados")
ValueError: 'type' must be in ['Fire', 'Water', 'Electric', 'Poison'] (got 'Gyarados')
```

Whether to choose attrs or dataclasses - it ultimately depends on your specific use case and if you are able to use 3rd party packages in your project. 

More on attrs:
* https://www.attrs.org/en/stable/
* [here's a great comparison of dataclasses and attrs](https://www.revsys.com/tidbits/dataclasses-and-attrs-when-and-why/) 

### pydantic

* FastAPI uses pydantic for schema definition and data validation

* pydantic enforces type hints at runtime

* the recommended way for creating pydantic models is to subclass `pydantic.BaseModel`, therefore all models inherit some methods:
```python
from pydantic import BaseModel

class PokemonDto(BaseModel):
    name: str
    type: str

    class Config:
        allow_mutation = False


# enforced keyword arguments in case of BaseModel subclass
pokemondto = PokemonDto(name="Charizard", type="Fire")
```

* like attrs, pydantic also supports vanilla Python dataclasses:

```python
import pydantic

@pydantic.dataclasses.dataclass(frozen=True)
class PokemonDto:
    name: str
    type: str

# in this case positional args are allowed
PokemonDto("Charizard", "Fire")
``` 

* enables (recursive) data validation:

```python
from enum import Enum

from pydantic import BaseModel

class TypeEnum(str, Enum):
    fire = "Fire"
    water = "Water"
    electric = "Electric"
    poison = "Poison"
    # ...


class PokemonDto(pydantic.BaseModel):
    name: str
    type: TypeEnum

    class Config:
        allow_mutation = False


>>> PokemonDto(name="Charizard", type="Fire")
PokemonDto(name='Charizard', type=<TypeEnum.fire: 'Fire'>)

>>> PokemonDto(name="Charizard", type="Charmeleon")
ValidationError: 1 validation error for PokemonDto
type
  value is not a valid enumeration member; permitted: 'Fire', 'Water', 'Electric', 'Poison' (type=type_error.enum; enum_values=[<TypeEnum.fire: 'Fire'>, <TypeEnum.water: 'Water'>, <TypeEnum.electric: 'Electric'>, <TypeEnum.poison: 'Poison'>])
```

* enables JSON (de)serialization:

```python
>>> PokemonDto(name="Charizard", type="Fire").json()
'{"name": "Charizard", "type": "Fire"}'
```

More on pydantic:
* https://pydantic-docs.helpmanual.io/


## Summary

Your choice on how to implement the DTOs depend on multiple circumstances - whether you need, among others:

* immutability
* default values support
* iterability
* serialization
* runtime type checking
* performance optimization
* other, more advanced configurability.

Post initially inspired by [this](https://www.reddit.com/r/Python/comments/ecw5gq/dtos_in_python/) Reddit thread.