4.2. OOP Dataclass

4.2.1. Rationale

  • Used for easier class definition

  • Since Python 3.7: PEP 557 -- Data Classes

  • Backported to Python 3.6 via python3 -m pip install dataclasses

4.2.2. Syntax

  • This are not static fields!

  • Dataclasses require Type Annotations

>>> class Point:
...     def __init__(self, x, y, z=0):
...         self.x = x
...         self.y = y
...         self.z = z
>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class Point:
...     x: int
...     y: int
...     z: int = 0

4.2.3. Example 1

class:

>>> class Point:
...     def __init__(self, x, y, z=0):
...         self.x = x
...         self.y = y
...         self.z = z
>>>
>>>
>>> p0 = Point()
Traceback (most recent call last):
TypeError: __init__() missing 2 required positional arguments: 'x' and 'y'
>>>
>>> p1 = Point(10)
Traceback (most recent call last):
TypeError: __init__() missing 1 required positional argument: 'y'
>>>
>>> p2 = Point(10, 20)
>>> p3 = Point(10, 20, 30)
>>> p4 = Point(10, 20, z=30)
>>> p5 = Point(10, 20, z=30)
>>> p6 = Point(x=10, y=20, z=30)

dataclass:

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Point:
...     x: int
...     y: int
...     z: int = 0
>>>
>>>
>>> p0 = Point()
Traceback (most recent call last):
TypeError: __init__() missing 2 required positional arguments: 'x' and 'y'
>>>
>>> p1 = Point(10)
Traceback (most recent call last):
TypeError: __init__() missing 1 required positional argument: 'y'
>>>
>>> p2 = Point(10, 20)
>>> p3 = Point(10, 20, 30)
>>> p4 = Point(10, 20, z=30)
>>> p5 = Point(10, 20, z=30)
>>> p6 = Point(x=10, y=20, z=30)

4.2.4. Example 2

class:

>>> class Astronaut:
...     firstname: str
...     lastname: str
...
...     def __init__(self, firstname: str, lastname: str, agency: str = 'POLSA'):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.agency = agency
>>>
>>>
>>> twardowski = Astronaut('Jan', 'Twardowski')
>>>
>>> print(twardowski.firstname)
Jan
>>> print(twardowski.lastname)
Twardowski
>>> print(twardowski.agency)
POLSA

dataclass:

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     agency: str = 'POLSA'
>>>
>>>
>>> twardowski = Astronaut('Jan', 'Twardowski')
>>>
>>> print(twardowski.firstname)
Jan
>>> print(twardowski.lastname)
Twardowski
>>> print(twardowski.agency)
POLSA

4.2.5. Example 3

>>> from dataclasses import dataclass
>>> from datetime import date
>>> from typing import Final, Optional
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     date_of_birth: date = date.today()
...     height: Optional[int] = None
...     friends: Optional[list['Astronaut']] = None
...     AGE_MIN: Final[int] = 27
...     AGE_MAX: Final[int] = 42
>>>
>>>
>>> Astronaut('Mark', 'Watney', date(1994, 10, 12))
Astronaut(firstname='Mark', lastname='Watney', date_of_birth=datetime.date(1994, 10, 12), height=None, friends=None, AGE_MIN=27, AGE_MAX=42)
>>>
>>> astro = Astronaut('Mark', 'Watney', date(1994, 10, 12), friends=[
...         Astronaut('Melissa', 'Lewis', date(1995, 7, 15)),
...         Astronaut('Rick', 'Martinez', date(1996, 1, 21)),
...         Astronaut('Beth', 'Johansen', date(2006, 5, 9)),
...         Astronaut('Chris', 'Beck', date(1999, 8, 2)),
...         Astronaut('Alex', 'Vogel', date(1994, 11, 15))])
>>>
>>> astro  
Astronaut(firstname='Mark', lastname='Watney', date_of_birth=datetime.date(1994, 10, 12), height=None, friends=[Astronaut(firstname='Melissa', lastname='Lewis', date_of_birth=datetime.date(1995, 7, 15), height=None, friends=None, AGE_MIN=27, AGE_MAX=42), Astronaut(firstname='Rick', lastname='Martinez', date_of_birth=datetime.date(1996, 1, 21), height=None, friends=None, AGE_MIN=27, AGE_MAX=42), Astronaut(firstname='Beth', lastname='Johansen', date_of_birth=datetime.date(2006, 5, 9), height=None, friends=None, AGE_MIN=27, AGE_MAX=42), Astronaut(firstname='Chris', lastname='Beck', date_of_birth=datetime.date(1999, 8, 2), height=None, friends=None, AGE_MIN=27, AGE_MAX=42), Astronaut(firstname='Alex', lastname='Vogel', date_of_birth=datetime.date(1994, 11, 15), height=None, friends=None, AGE_MIN=27, AGE_MAX=42)], AGE_MIN=27, AGE_MAX=42)

4.2.6. Example 4

class:

>>> from datetime import datetime
>>>
>>>
>>> class StarWarsMovie:
...     title: str
...     episode_id: int
...     opening_crawl: str
...     director: str
...     producer: str
...     release_date: datetime
...     characters: tuple[str]
...     planets: tuple[str]
...     starships: tuple[str]
...     vehicles: tuple[str]
...     species: tuple[str]
...     created: datetime
...     edited: datetime
...     url: str
...
...     def __init__(self, title: str, episode_id: int, opening_crawl: str,
...                  director: str, producer: str, release_date: datetime,
...                  characters: tuple[str], planets: tuple[str], starships: tuple[str],
...                  vehicles: tuple[str], species: tuple[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

dataclass:

>>> from dataclasses import dataclass
>>> from datetime import datetime
>>>
>>>
>>> @dataclass
... class StarWarsMovie:
...     title: str
...     episode_id: int
...     opening_crawl: str
...     director: str
...     producer: str
...     release_date: datetime
...     characters: tuple[str]
...     planets: tuple[str]
...     starships: tuple[str]
...     vehicles: tuple[str]
...     species: tuple[str]
...     created: datetime
...     edited: datetime
...     url: str

4.2.7. __init__ vs. __post_init__

class:

>>> class Kelvin:
...     def __init__(self, value):
...         if value < 0.0:
...             raise ValueError('Temperature must be greater than 0')
...         else:
...             self.value = value
>>>
>>>
>>> t = Kelvin(-1)
Traceback (most recent call last):
ValueError: Temperature must be greater than 0

dataclass:

>>> 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')
>>>
>>>
>>> t = Kelvin(-1)
Traceback (most recent call last):
ValueError: Temperature must be greater than 0
>>> from dataclasses import dataclass, field
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     publicname: str = field(init=False)
...
...     def __post_init__(self):
...         self.publicname = f'{self.firstname} {self.lastname[0]}.'

4.2.8. Field Object

  • name - The name of the field.

  • type - The type of the field.

  • default - Default value

  • default_factory - Field factory

  • init

  • repr

  • hash

  • compare

  • metadata - This can be a mapping or None. None is treated as an empty dict. It is not used at all by Data Classes, and is provided as a third-party extension mechanism.

>>> from dataclasses import dataclass, field
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     publicname: int = field(repr=False)
...     agency: int = field(repr=False, default='NASA')
>>> from __future__ import annotations
>>> from dataclasses import dataclass, field
>>>
>>>
>>> @dataclass(frozen=True)
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     age: float
...     height: float = field(metadata={'unit': 'cm'})
...     weight: float = field(metadata={'unit': 'km'})
...     mission: list[Mission]
...     agency: str = field(default='NASA', metadata={'choices': ['NASA', 'ESA']})
...     friends: list[Astronaut] = field(default_factory=list)
...     country: str = 'USA'
...
...     def __post_init__(self):
...         if self.age > 65:
...             raise ValueError('Too old for an astronaut')
...
...     def say_hello(self):
...         print(f'Howdy, I am {self.firstname} {self.lastname}')
...
...
>>> astro = Astronaut('Mark', 'Watney',
...                   age=44,
...                   height=170,
...                   weight=75,
...                   mission=[Mission(2035, 'Ares 3')],
...                   friends=[],
...                   agency='NASA')
>>>
>>> astro
Astronaut(firstname='Mark', lastname='Watney', age=44, height=170, weight=75, mission=[Mission(year=2035, name='Ares 3')], agency='NASA', friends=[], country='USA')
>>>
>>> astro.__dataclass_fields__['agency'].metadata['choices']
['NASA', 'ESA']
>>> astro.__dataclass_fields__['height'].metadata['unit']
'cm'
>>> Astronaut.__dataclass_fields__['agency'].metadata['choices']
['NASA', 'ESA']

4.2.9. Mutable attributes

Warning

Note, You should not set mutable objects as a default function argument. More information in Argument Mutability

>>> class Astronaut:
...     def __init__(self, name, missions=[]):
...         self.name = name
...         self.missions = missions
>>>
>>>
>>> watney = Astronaut('Mark Watney')
>>> twardowski = Astronaut('Jan Twardowski')
>>>
>>> watney.missions.append('Ares 1')
>>> watney.missions.append('Ares 2')
>>> watney.missions.append('Ares 3')
>>> watney.missions.append('Ares 4')
>>> watney.missions.append('Ares 5')
>>>
>>> print('Watney:', watney.missions)
Watney: ['Ares 1', 'Ares 2', 'Ares 3', 'Ares 4', 'Ares 5']
>>>
>>> print('Twardowski:', twardowski.missions)
Twardowski: ['Ares 1', 'Ares 2', 'Ares 3', 'Ares 4', 'Ares 5']
>>> from dataclasses import dataclass, field
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     missions: dict[int,str] = field(default_factory=dict)
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>> from dataclasses import dataclass, field
>>>
>>>
>>> @dataclass
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     missions: list[Mission] = field(default_factory=list)
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')

4.2.10. Dataclass parameters

Todo

Table is more readable

  • init - Generate __init__() method (default True)

  • repr - Generate __repr__() method (default True)

  • eq - Generate __eq__() and __ne__() methods (default True)

  • order - Generate __lt__(), __le__(), __gt__(), and __ge__() methods (default False)

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

  • frozen - if True: assigning to fields will generate an exception (default False)

Table 4.1. Dataclass options

Option

Default

Description (if True)

init

True

Generate __init__() method

repr

True

Generate __repr__() method

eq

True

Generate __eq__() and __ne__() methods

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

4.2.11. Init

  • Generate __init__() method

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(init=False)
... class Point:
...     x: int
...     y: int
>>>
>>>
>>> p = Point(10, 20)
Traceback (most recent call last):
TypeError: Point() takes no arguments

4.2.12. Repr

  • repr=True by default

  • Generate __repr__() for pretty printing

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(repr=True)
... class Point:
...     x: int
...     y: int
>>>
>>>
>>> p = Point(10, 20)
>>>
>>> print(p)
Point(x=10, y=20)
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(repr=False)
... class Point:
...     x: int
...     y: int
>>>
>>>
>>> p = Point(10, 20)
>>>
>>> print(p)  
<Point object at 0x...>

4.2.13. Frozen

  • frozen=False by default

  • Prevents object from modifications

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(frozen=True)
... class Point:
...     x: int
...     y: int
>>>
>>>
>>> p = Point(10, 20)
>>>
>>> p.x = 30
Traceback (most recent call last):
dataclasses.FrozenInstanceError: cannot assign to field 'x'

4.2.14. Eq

  • eq=True by default

  • when eq=False compare objects by id() not values

  • when eq=True compare objects by value not id()

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(eq=True)
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> astro1 = Astronaut('Mark', 'Watney')
>>> astro2 = Astronaut('Mark', 'Watney')
>>> astro3 = Astronaut('Jan', 'Twardowski')
>>>
>>> astro1 == astro1
True
>>> astro1 == astro2
True
>>> astro1 == astro3
False
>>>
>>> astro1 != astro1
False
>>> astro1 != astro2
False
>>> astro1 != astro3
True
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(eq=False)
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> astro1 = Astronaut('Mark', 'Watney')
>>> astro2 = Astronaut('Mark', 'Watney')
>>> astro3 = Astronaut('Jan', 'Twardowski')
>>>
>>> astro1 == astro1
True
>>> astro1 == astro2
False
>>> astro1 == astro3
False
>>>
>>> astro1 != astro1
False
>>> astro1 != astro2
True
>>> astro1 != astro3
True

4.2.15. Other flags

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>> astro1 = Astronaut('Mark', 'Watney')
>>> astro2 = Astronaut('Mark', 'Watney')
>>> astro3 = Astronaut('Jan', 'Twardowski')

4.2.16. InitVar

  • Init-only fields are added as parameters to the generated __init__ method, and are passed to the optional __post_init__ method

  • They are not otherwise used by Data Classes

>>> 
... from dataclasses import dataclass, InitVar
...
...
... @dataclass
... class Astronaut:
...     fullname: InitVar[str] = None
...     _firstname: str = None
...     _lastname: str = None
...
...     def __post_init__(self, fullname: str):
...         fullname = fullname.split()
...         self._firstname = fullname[0]
...         self._lastname = fullname[1]
...
...
... astro = Astronaut('Mark Watney')
...
... print(astro._firstname)
Mark
... print(astro._lastname)
Watney

4.2.17. Inheritance

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Person:
...     name: str
...     job: str = None
>>>
>>>
>>> @dataclass
... class Astronaut(Person):
...     job: str = 'Astronaut'
...     agency: str = 'NASA'
>>>
>>>

Will generate def __init__(self, name: str, job: str = 'Astronaut', agency: str = 'NASA')

4.2.18. Helper functions

  • fields(class_or_instance) - Returns a tuple of Field objects that define the fields for this Data Class. Accepts either a Data Class, or an instance of a Data Class. Raises ValueError if not passed a Data Class or instance of one. Does not return pseudo-fields which are ClassVar or InitVar.

  • asdict(instance, *, dict_factory=dict) - Converts the Data Class instance to a dict (by using the factory function dict_factory)

  • astuple(*, tuple_factory=tuple) - Converts the Data Class instance to a tuple (by using the factory function tuple_factory). Each Data Class is converted to a tuple of its field values. Data Classes, dicts, lists, and tuples are recursed into.

  • make_dataclass(cls_name, fields, *, bases=(), namespace=None) - Creates a new Data Class with name cls_name, fields as defined in fields, base classes as given in bases, and initialized with a namespace as given in namespace.

  • replace(instance, **changes) - Creates a new object of the same type of instance, replacing fields with values from changes. If instance is not a Data Class, raises TypeError. If values in changes do not specify fields, raises TypeError.

  • is_dataclass(class_or_instance) - Returns True if its parameter is a dataclass or an instance of one, otherwise returns False.

>>> from dataclasses import dataclass, asdict, astuple
>>>
>>>
>>> @dataclass
... class Point:
...     x: int
...     y: int
>>>
>>> @dataclass
... class Coordinates:
...     points: list[Point]
>>>
>>>
>>> p = Point(10, 20)
>>> c = Coordinates([Point(0, 0), Point(10, 4)])
>>>
>>> asdict(p)
{'x': 10, 'y': 20}
>>> asdict(c)
{'points': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
>>>
>>> astuple(p)
(10, 20)
>>> astuple(c)
([(0, 0), (10, 4)],)

4.2.19. Under the hood

Your code:

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

Dataclass will generate:

>>> class ShoppingCartItem:
...     name: str
...     unit_price: float
...     quantity: int
...
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
...
...     ## All code below is added by dataclass
...
...     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

4.2.20. Use Cases

>>> from dataclasses import dataclass
>>>
>>>
>>> DATA = [('Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'),
...         (5.8, 2.7, 5.1, 1.9, 'virginica'),
...         (5.1, 3.5, 1.4, 0.2, 'setosa'),
...         (5.7, 2.8, 4.1, 1.3, 'versicolor'),
...         (6.3, 2.9, 5.6, 1.8, 'virginica'),
...         (6.4, 3.2, 4.5, 1.5, 'versicolor'),
...         (4.7, 3.2, 1.3, 0.2, 'setosa')]
>>>
>>>
>>> @dataclass
... class Iris:
...     sepal_length: float
...     sepal_width: float
...     petal_length: float
...     petal_width: float
...     species: str
>>>
>>>
>>> flowers = list(Iris(*row) for row in DATA[1:])
>>> print(flowers)  
[Iris(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9, species='virginica'),
 Iris(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2, species='setosa'),
 Iris(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3, species='versicolor'),
 Iris(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8, species='virginica'),
 Iris(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5, species='versicolor'),
 Iris(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2, species='setosa')]

4.2.21. Assignments

Code 4.1. Solution
"""
* Assignment: OOP Dataclass Syntax
* Complexity: easy
* Lines of code: 7 lines
* Time: 5 min

English:
    1. Use Dataclass to define class `Point` with attributes:
        a. `x: int` with default value `0`
        b. `y: int` with default value `0`
    2. When `x` or `y` has negative value raise en exception `ValueError('Coordinate cannot be negative')`
    3. Use `datalass` and validation in `__post_init__()`
    4. Run doctests - all must succeed

Polish:
    1. Użyj Dataclass do zdefiniowania klasy `Point` z atrybutami:
        a. `x: int` z domyślną wartością `0`
        b. `y: int` z domyślną wartością `0`
    2. Gdy `x` lub `y` mają wartość ujemną podnieś wyjątek `ValueError('Coordinate cannot be negative')`
    3. Użyj `datalass` i walidacji w `__post_init__()`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Point)
    >>> assert hasattr(Point, 'x')
    >>> assert hasattr(Point, 'y')

    >>> Point()
    Point(x=0, y=0)

    >>> Point(x=0, y=0)
    Point(x=0, y=0)

    >>> Point(x=1, y=2)
    Point(x=1, y=2)

    >>> Point(x=-1, y=0)
    Traceback (most recent call last):
    ValueError: Coordinate cannot be negative

    >>> Point(x=0, y=-1)
    Traceback (most recent call last):
    ValueError: Coordinate cannot be negative
"""

from dataclasses import dataclass


Code 4.2. Solution
"""
* Assignment: OOP Dataclass Addressbook
* Complexity: easy
* Lines of code: 12 lines
* Time: 8 min

English:
    1. Model `DATA` using `dataclasses`
        a. Create class definition, fields and their types
        b. Do not write code converting `DATA` to your classes
    2. Fields should have deafault value set to empty `str`
    3. Run doctests - all must succeed

Polish:
    1. Zamodeluj `DATA` wykorzystując `dataclass`
        a. Stwórz definicję klas, pól i ich typów
        b. Nie pisz kodu konwertującego `DATA` do Twoich klas
    2. Pola mają mieć wartość domyślną pusty `str`
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(Astronaut)
    >>> assert isclass(Address)
    >>> assert hasattr(Astronaut, 'firstname')
    >>> assert hasattr(Astronaut, 'lastname')
    >>> assert hasattr(Address, 'street')
    >>> assert hasattr(Address, 'city')
    >>> assert hasattr(Address, 'post_code')
    >>> assert hasattr(Address, 'region')
    >>> assert hasattr(Address, 'country')
"""

from dataclasses import dataclass, field


DATA = [
    {"firstname": "Jan", "lastname": "Twardowski", "addresses": [
        {"street": "Kamienica Pod św. Janem Kapistranem", "city": "Kraków", "post_code": "31-008", "region": "Małopolskie", "country": "Poland"}]},

    {"firstname": "José", "lastname": "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"}]},

    {"firstname": "Mark", "lastname": "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"}]},

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

    {"firstname": "Melissa", "lastname": "Lewis"},

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


Code 4.3. Solution
"""
* Assignment: OOP Dataclass JSON
* Complexity: medium
* Lines of code: 23 lines
* Time: 21 min

English:
    1. You received input data in JSON format from the API
        a. `str` fields: firstname, lastname, role, username, password, email,
        b. `datetime` fields: date_of_birth, last_login,
        c. `bool` fields: is_active, is_staff, is_superuser,
        d. `list[dict]` field: user_permissions
    2. Using `dataclass` model data as class `User`
        a. Note, that fields order is important for tests to pass
    3. Parse fields with dates and store as `datetime` objects
    4. Parse fields with `true` and `false` values and store as `bool` objects
    5. Do not create additional classes to represent `permission` filed, leave it as `list[dict]`
    6. Iterate over records and create instances of this class
    7. Collect all instances to `result: list[User]`
    8. Run doctests - all must succeed

Polish:
    1. Otrzymałeś z API dane wejściowe w formacie JSON
        a. pola `str`: firstname, lastname, role, username, password, email,
        b. pola `datetime`: date_of_birth, last_login,
        c. pola `bool`: is_active, is_staff, is_superuser,
        d. pola `list[dict]`: user_permissions
    2. Wykorzystując `dataclass` zamodeluj dane za pomocą klasy `User`
        a. Zwróć uwagę, że kolejność pól ma znaczenie aby testy przechodziły
    3. Sparsuj pola zwierające daty i zapisz je jako obiekty `datetime`
    4. Sparsuj pola zawierające `true` lub `false` i zapamiętaj ich wartości jako obiekty `bool`
    5. Nie twórz dodatkowych klas do reprezentacji pola `permission`, niech zostanie jako `list[dict]`
    6. Iterując po rekordach twórz instancje tej klasy
    7. Zbierz wszystkie instancje do `result: list[User]`
    8. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [User(firstname='Melissa', lastname='Lewis', role='commander', username='mlewis', password='pbkdf2_sha256$120000$gvEBNiCeTrYa0$5C+NiCeTrYsha1PHogqvXNiCeTrY0CRSLYYAA90=', email='melissa.lewis@nasa.gov', date_of_birth=datetime.date(1995, 7, 15), last_login=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), is_active=True, is_staff=True, is_superuser=False, user_permissions=[{'eclss': ['add', 'modify', 'view']}, {'communication': ['add', 'modify', 'view']}, {'medical': ['add', 'modify', 'view']}, {'science': ['add', 'modify', 'view']}]),
     User(firstname='Rick', lastname='Martinez', role='pilot', username='rmartinez', password='pbkdf2_sha256$120000$aXNiCeTrY$UfCJrBh/qhXohNiCeTrYH8nsdANiCeTrYnShs9M/c=', email='rick.martinez@ansa.gov', date_of_birth=datetime.date(1996, 1, 21), last_login=None, is_active=True, is_staff=True, is_superuser=False, user_permissions=[{'communication': ['add', 'view']}, {'eclss': ['add', 'modify', 'view']}, {'science': ['add', 'modify', 'view']}]),
     User(firstname='Alex', lastname='Vogel', role='chemist', username='avogel', password='pbkdf2_sha256$120000$eUNiCeTrYHoh$X32NiCeTrYZOWFdBcVT1l3NiCeTrY4WJVhr+cKg=', email='alex.vogel@esa.int', date_of_birth=datetime.date(1994, 11, 15), last_login=None, is_active=True, is_staff=True, is_superuser=False, user_permissions=[{'eclss': ['add', 'modify', 'view']}, {'communication': ['add', 'modify', 'view']}, {'medical': ['add', 'modify', 'view']}, {'science': ['add', 'modify', 'view']}]),
     User(firstname='Chris', lastname='Beck', role='crew-medical-officer', username='cbeck', password='pbkdf2_sha256$120000$3G0RNiCeTrYlaV1$mVb62WNiCeTrYQ9aYzTsSh74NiCeTrY2+c9/M=', email='chris.beck@nasa.gov', date_of_birth=datetime.date(1999, 8, 2), last_login=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), is_active=True, is_staff=True, is_superuser=False, user_permissions=[{'communication': ['add', 'view']}, {'medical': ['add', 'modify', 'view']}, {'science': ['add', 'modify', 'view']}]),
     User(firstname='Beth', lastname='Johansen', role='sysop', username='bjohansen', password='pbkdf2_sha256$120000$QmSNiCeTrYBv$Nt1jhVyacNiCeTrYSuKzJ//WdyjlNiCeTrYYZ3sB1r0g=', email='', date_of_birth=datetime.date(2006, 5, 9), last_login=None, is_active=True, is_staff=True, is_superuser=False, user_permissions=[{'communication': ['add', 'view']}, {'science': ['add', 'modify', 'view']}]),
     User(firstname='Mark', lastname='Watney', role='botanist', username='mwatney', password='pbkdf2_sha256$120000$bxS4dNiCeTrY1n$Y8NiCeTrYRMa5bNJhTFjNiCeTrYp5swZni2RQbs=', email='', date_of_birth=datetime.date(1994, 10, 12), last_login=None, is_active=True, is_staff=True, is_superuser=False, user_permissions=[{'communication': ['add', 'modify', 'view']}, {'science': ['add', 'modify', 'view']}])]
"""

import json
from dataclasses import dataclass
from datetime import date, datetime, timezone
from typing import Optional, Union

DATA = '[{"model":"authorization.user","pk":1,"fields":{"firstname":"Melissa","lastname":"Lewis","role":"commander","username":"mlewis","password":"pbkdf2_sha256$120000$gvEBNiCeTrYa0$5C+NiCeTrYsha1PHogqvXNiCeTrY0CRSLYYAA90=","email":"melissa.lewis@nasa.gov","date_of_birth":"1995-07-15","last_login":"1970-01-01T00:00:00.000Z","is_active":true,"is_staff":true,"is_superuser":false,"user_permissions":[{"eclss":["add","modify","view"]},{"communication":["add","modify","view"]},{"medical":["add","modify","view"]},{"science":["add","modify","view"]}]}},{"model":"authorization.user","pk":2,"fields":{"firstname":"Rick","lastname":"Martinez","role":"pilot","username":"rmartinez","password":"pbkdf2_sha256$120000$aXNiCeTrY$UfCJrBh/qhXohNiCeTrYH8nsdANiCeTrYnShs9M/c=","date_of_birth":"1996-01-21","last_login":null,"email":"rick.martinez@ansa.gov","is_active":true,"is_staff":true,"is_superuser":false,"user_permissions":[{"communication":["add","view"]},{"eclss":["add","modify","view"]},{"science":["add","modify","view"]}]}},{"model":"authorization.user","pk":3,"fields":{"firstname":"Alex","lastname":"Vogel","role":"chemist","username":"avogel","password":"pbkdf2_sha256$120000$eUNiCeTrYHoh$X32NiCeTrYZOWFdBcVT1l3NiCeTrY4WJVhr+cKg=","email":"alex.vogel@esa.int","date_of_birth":"1994-11-15","last_login":null,"is_active":true,"is_staff":true,"is_superuser":false,"user_permissions":[{"eclss":["add","modify","view"]},{"communication":["add","modify","view"]},{"medical":["add","modify","view"]},{"science":["add","modify","view"]}]}},{"model":"authorization.user","pk":4,"fields":{"firstname":"Chris","lastname":"Beck","role":"crew-medical-officer","username":"cbeck","password":"pbkdf2_sha256$120000$3G0RNiCeTrYlaV1$mVb62WNiCeTrYQ9aYzTsSh74NiCeTrY2+c9/M=","email":"chris.beck@nasa.gov","date_of_birth":"1999-08-02","last_login":"1970-01-01T00:00:00.000Z","is_active":true,"is_staff":true,"is_superuser":false,"user_permissions":[{"communication":["add","view"]},{"medical":["add","modify","view"]},{"science":["add","modify","view"]}]}},{"model":"authorization.user","pk":5,"fields":{"firstname":"Beth","lastname":"Johansen","role":"sysop","username":"bjohansen","password":"pbkdf2_sha256$120000$QmSNiCeTrYBv$Nt1jhVyacNiCeTrYSuKzJ//WdyjlNiCeTrYYZ3sB1r0g=","email":"","date_of_birth":"2006-05-09","last_login":null,"is_active":true,"is_staff":true,"is_superuser":false,"user_permissions":[{"communication":["add","view"]},{"science":["add","modify","view"]}]}},{"model":"authorization.user","pk":6,"fields":{"firstname":"Mark","lastname":"Watney","role":"botanist","username":"mwatney","password":"pbkdf2_sha256$120000$bxS4dNiCeTrY1n$Y8NiCeTrYRMa5bNJhTFjNiCeTrYp5swZni2RQbs=","email":"","date_of_birth":"1994-10-12","last_login":null,"is_active":true,"is_staff":true,"is_superuser":false,"user_permissions":[{"communication":["add","modify","view"]},{"science":["add","modify","view"]}]}}]'  # noqa


def _clean_time(text: str) -> Union[datetime,date,None]:
    if not text:
        return None

    try:
        return datetime.strptime(text, '%Y-%m-%d').date()
    except ValueError:
        pass

    try:
        return datetime.strptime(text, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc)
    except TypeError:
        pass