Some ideas on how to implement DTOs in Python

Data Transfer Objects 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:

itemdto = {
“name”: “Potion”,
“location”: Location(“V…


This content originally appeared on DEV Community and was authored by Izabela Kowal

Data Transfer Objects 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:

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:

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:

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:

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:

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
>>> 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
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:
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
@dataclass(frozen=True)
class ItemDto:
    name: str
    location: Location
    description: str = ""
  • not iterable
...: for field in itemdto:
...:     print(field)
...: 
TypeError: 'ItemDto' object is not iterable

More on dataclasses:

NamedTuples

  • NamedTuple is a subclass of regular tuple
  • introduced in Python 3.0 as a factory method in the collections module:
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:
from typing import NamedTuple

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


# supports 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
...: for field in itemdto:
...:     print(field)
...: 
X Speed
Temporarily raise Speed in battle
Location(name=Celadon Dept. Store)
  • disadvantage: fields with a default value must come after any fields without a default.

More on NamedTuples:

TypedDicts

  • available since Python 3.8:
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:

3rd party packages

attrs

  • a dependency for pytest, 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:
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:

@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:

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:

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:
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:
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:
>>> PokemonDto(name="Charizard", type="Fire").json()
'{"name": "Charizard", "type": "Fire"}'

More on pydantic:

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 Reddit thread.


This content originally appeared on DEV Community and was authored by Izabela Kowal


Print Share Comment Cite Upload Translate Updates
APA

Izabela Kowal | Sciencx (2021-04-19T21:54:44+00:00) Some ideas on how to implement DTOs in Python. Retrieved from https://www.scien.cx/2021/04/19/some-ideas-on-how-to-implement-dtos-in-python/

MLA
" » Some ideas on how to implement DTOs in Python." Izabela Kowal | Sciencx - Monday April 19, 2021, https://www.scien.cx/2021/04/19/some-ideas-on-how-to-implement-dtos-in-python/
HARVARD
Izabela Kowal | Sciencx Monday April 19, 2021 » Some ideas on how to implement DTOs in Python., viewed ,<https://www.scien.cx/2021/04/19/some-ideas-on-how-to-implement-dtos-in-python/>
VANCOUVER
Izabela Kowal | Sciencx - » Some ideas on how to implement DTOs in Python. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/04/19/some-ideas-on-how-to-implement-dtos-in-python/
CHICAGO
" » Some ideas on how to implement DTOs in Python." Izabela Kowal | Sciencx - Accessed . https://www.scien.cx/2021/04/19/some-ideas-on-how-to-implement-dtos-in-python/
IEEE
" » Some ideas on how to implement DTOs in Python." Izabela Kowal | Sciencx [Online]. Available: https://www.scien.cx/2021/04/19/some-ideas-on-how-to-implement-dtos-in-python/. [Accessed: ]
rf:citation
» Some ideas on how to implement DTOs in Python | Izabela Kowal | Sciencx | https://www.scien.cx/2021/04/19/some-ideas-on-how-to-implement-dtos-in-python/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.