9. Dataclass

9.1. Syntax

  • This are not static fields!

  • Dataclasses require Type Annotations

  • Introduced in Pyton 3.7

  • Backported to Python 3.6 via pip install dataclasses

9.2. Example

Listing 272. Defining classes
class Point:
    def __init__(self, x=0, y=0, z=0):
        self.x = x
        self.y = y
        self.z = z

p = Point(x=10, y=20, z=30)
Listing 273. Defining dataclasses
from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int
    z: int

p = Point(x=10, y=20, z=30)

9.2.1. Example 2

Listing 274. Defining classes
class Astronaut:
    def __init__(self, first_name: str, last_name: str, agency: str = 'POLSA'):
        self.first_name = first_name
        self.last_name = last_name
        self.agency = agency


twardowski = Astronaut(first_name='Jan', last_name='Twardowski')

twardowski.first_name   # Jan
twardowski.last_name    # Twardowski
twardowski.agency       # POLSA
Listing 275. Defining dataclasses
from dataclasses import dataclass


@dataclass
class Astronaut:
    first_name: str
    last_name: str
    agency: str = 'POLSA'


twardowski = Astronaut(first_name='Jan', last_name='Twardowski')

twardowski.first_name   # Jan
twardowski.last_name    # Twardowski
twardowski.agency       # POLSA

9.3. __init__ vs. __post_init__

9.3.1. Classes

class Kelvin:
    def __init__(self, value):
        if self.value < 0.0:
            raise ValueError('Temperature must be greater than 0')
        else:
            self.value = value


temp = Kelvin(-300)

9.3.2. Dataclasses

from dataclasses import dataclass


@dataclass
class Kelvin:
    value: float = 0.0

    def __post_init__(self):
        if self.value < 0.0:
            raise ValueError('Temperature must be greater than 0')


temp = Kelvin(-300)

9.4. Field Factory

from dataclasses import dataclass

@dataclass
class C:
    x: int
    y: int = field(repr=False)
    z: int = field(repr=False, default=10)
    t: int = 20
from dataclasses import dataclass

@dataclass
class C:
    mylist: List[int] = field(default_factory=list)

c = C()
c.mylist += [1, 2, 3]

9.4.1. Why?

class Contact:
    def __init__(self, name, addresses=[]):
        self.name = name
        self.addresses = addresses


twardowski = Contact(name='Jan Twardowski')
twardowski.addresses.append('Johnson Space Center')
print(twardowski.addresses)
# [Johnson Space Center]

watney = Contact(name='Mark Watney')
print(watney.addresses)
# [Johnson Space Center]

9.4.2. So what?

  • field() creates new empty list for each object

  • It does not reuse pointer

9.5. Use cases

9.5.1. Old style classes

class StarWarsMovie:

    def __init__(self, title: str, episode_id: int, opening_crawl: str,
                 director: str, producer: str, release_date: datetime,
                 characters: List[str], planets: List[str], starships: List[str],
                 vehicles: List[str], species: List[str], created: datetime,
                 edited: datetime, url: str):

        self.title = title
        self.episode_id = episode_id
        self.opening_crawl= opening_crawl
        self.director = director
        self.producer = producer
        self.release_date = release_date
        self.characters = characters
        self.planets = planets
        self.starships = starships
        self.vehicles = vehicles
        self.species = species
        self.created = created
        self.edited = edited
        self.url = url

        if type(self.release_date) is str:
            self.release_date = dateutil.parser.parse(self.release_date)

        if type(self.created) is str:
            self.created = dateutil.parser.parse(self.created)

        if type(self.edited) is str:
            self.edited = dateutil.parser.parse(self.edited)

9.5.2. Dataclasses

from dataclasses import dataclass


@dataclass
class StarWarsMovie:
    title: str
    episode_id: int
    opening_crawl: str
    director: str
    producer: str
    release_date: datetime
    characters: List[str]
    planets: List[str]
    starships: List[str]
    vehicles: List[str]
    species: List[str]
    created: datetime
    edited: datetime
    url: str

    def __post_init__(self):
        if type(self.release_date) is str:
            self.release_date = dateutil.parser.parse(self.release_date)

        if type(self.created) is str:
            self.created = dateutil.parser.parse(self.created)

        if type(self.edited) is str:
            self.edited = dateutil.parser.parse(self.edited)

9.6. More advanced options

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
Table 39. More advanced options

Option

Default

Description (if True)

init

True

Generate __init__() method

repr

True

Generate __repr__() method

eq

True

Generate __eq__() method

order

False

Generate __lt__(), __le__(), __gt__(), and __ge__() methods

unsafe_hash

False

if False: the __hash__() method is generated according to how eq and frozen are set

frozen

False

if True: assigning to fields will generate an exception

9.7. Under the hood

9.7.1. Write

from dataclasses import dataclass

@dataclass
class ShoppingCartItem:
    name: str
    unit_price: float
    quantity: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity

9.7.2. Dataclass will add

class ShoppingCartItem:

    def total_cost(self) -> float:
        return self.unit_price * self.quantity

    def __init__(self, name: str, unit_price: float, quantity: int = 0) -> None:
        self.name = name
        self.unit_price = unit_price
        self.quantity = quantity

    def __repr__(self):
        return f'ShoppingCartItem(name={self.name!r}, unit_price={self.unit_price!r}, quantity={self.quantity!r})'

    def __eq__(self, other):
        if other.__class__ is self.__class__:
            return (self.name, self.unit_price, self.quantity) == (other.name, other.unit_price, other.quantity)
        return NotImplemented

    def __ne__(self, other):
        if other.__class__ is self.__class__:
            return (self.name, self.unit_price, self.quantity) != (other.name, other.unit_price, other.quantity)
        return NotImplemented

    def __lt__(self, other):
        if other.__class__ is self.__class__:
            return (self.name, self.unit_price, self.quantity) < (other.name, other.unit_price, other.quantity)
        return NotImplemented

    def __le__(self, other):
        if other.__class__ is self.__class__:
            return (self.name, self.unit_price, self.quantity) <= (other.name, other.unit_price, other.quantity)
        return NotImplemented

    def __gt__(self, other):
        if other.__class__ is self.__class__:
            return (self.name, self.unit_price, self.quantity) > (other.name, other.unit_price, other.quantity)
        return NotImplemented

    def __ge__(self, other):
        if other.__class__ is self.__class__:
            return (self.name, self.unit_price, self.quantity) >= (other.name, other.unit_price, other.quantity)
        return NotImplemented

9.8. Assignments

9.8.1. Address Book (dataclass)

English
  1. Model data using dataclasses

Polish
  1. Zamodeluj dane wykorzystując dataclass

Input
Listing 276. Data for AddressBook
[
    {"first_name": "Jan", "last_name": "Twardowski", "addresses": [
        {"street": "Kamienica Pod św. Janem Kapistranem", "city": "Kraków", "post_code": "31-008", "region": "Malopołskie", "country": "Poland"}]},

    {"first_name": "José", "last_name": "Jiménez", "addresses": [
        {"street": "2101 E NASA Pkwy", "city": "Houston", "post_code": 77058, "region": "Texas", "country": "USA"},
        {"street": "", "city": "Kennedy Space Center", "post_code": 32899, "region": "Florida", "country": "USA"}]},

    {"first_name": "Mark", "last_name": "Watney", "addresses": [
        {"street": "4800 Oak Grove Dr", "city": "Pasadena", "post_code": 91109, "region": "California", "country": "USA"},
        {"street": "2825 E Ave P", "city": "Palmdale", "post_code": 93550, "region": "California", "country": "USA"}]},

    {"first_name": "Иван", "last_name": "Иванович", "addresses": [
        {"street": "", "city": "Космодро́м Байкону́р", "post_code": "", "region": "Кызылординская область", "country": "Қазақстан"},
        {"street": "", "city": "Звёздный городо́к", "post_code": 141160, "region": "Московская область", "country": "Россия"}]},

    {"first_name": "Melissa", "last_name": "Lewis", "addresses": []},

    {"first_name": "Alex", "last_name": "Vogel", "addresses": [
        {"street": "Linder Hoehe", "city": "Köln", "post_code": 51147, "region": "North Rhine-Westphalia", "country": "Germany"}]}
]