6.3. OOP Dataclass

6.3.1. Syntax

  • This are not static fields!

  • Dataclasses require Type Annotations

  • Introduced in Python 3.7

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

6.3.2. 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)

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

6.3.4. Example 3

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

6.3.5. __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


t1 = Kelvin(273.15)

print(t1.value)
# 273.15

t2 = Kelvin(-10)
# 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')


t1 = Kelvin(273.15)

print(t1.value)
# 273.15

t2 = Kelvin(-10)
# Traceback (most recent call last):
# ValueError: Temperature must be greater than 0

6.3.6. Field Factory

from dataclasses import dataclass, field


@dataclass
class Point:
    x: int
    y: int = field(repr=False)
    z: int = field(repr=False, default=10)
    t: int = 20

6.3.7. List attributes

  • You should not set mutable objects as a default function argument

  • field() creates new empty list for each object

  • It does not reuse pointer

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')
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']

twardowski = Astronaut('Jan Twardowski')
print('Twardowski:', twardowski.missions)
# Twardowski: ['Ares 1', 'Ares 2', 'Ares 3', 'Ares 4', 'Ares 5']
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')

6.3.8. Dataclass parameters

Table 6.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

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

6.3.10. 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)
# <__main__.Point object at 0x110bf5550>

6.3.11. 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'

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

6.3.13. 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')

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

6.3.15. 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'),
        (7.0, 3.2, 4.7, 1.4, 'versicolor'),
        (7.6, 3.0, 6.6, 2.1, 'virginica'),
        (4.6, 3.1, 1.5, 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'),
#   Iris(sepal_length=7.0, sepal_width=3.2, petal_length=4.7, petal_width=1.4, species='versicolor'),
#   Iris(sepal_length=7.6, sepal_width=3.0, petal_length=6.6, petal_width=2.1, species='virginica'),
#   Iris(sepal_length=4.6, sepal_width=3.1, petal_length=1.5, petal_width=0.2, species='setosa')
# ]

6.3.16. Assignments

Code 6.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:
    >>> 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
"""


# Given
from dataclasses import dataclass


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

English:
    1. Use data from "Given" section (see below)
    2. Model data using `dataclasses`
    3. Create classes to represent `DATA`, but do not convert it
    4. Fields should have deafault value set to empty `str`
    5. Run doctests - all must succeed

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Zamodeluj dane wykorzystując `dataclass`
    3. Stwórz klasy do reprezentacji `DATA`, ale nie konwertuj tego
    4. Pola mają mieć wartość domyślną pusty `str`
    5. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> 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')
"""


# Given
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 6.3. Solution
"""
* Assignment: OOP Dataclass JSON
* Complexity: medium
* Lines of code: 23 lines
* Time: 21 min

English:
    1. Use data from "Given" section (see below)
    2. You received input data in JSON format from the API
    3. Using `dataclass` Model data as class `User`
    4. Parse fields with dates and store as `datetime` objects
    5. Parse fields with `true` and `false` values and store as `bool` objects
    6. 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 one list
    8. Run doctests - all must succeed

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Otrzymałeś z API dane wejściowe w formacie JSON
    3. Wykorzystując `dataclass` zamodeluj dane za pomocą klasy `User`
    4. Sparsuj pola zwierające daty i zapisz je jako obiekty `datetime`
    5. Sparsuj pola zawierające `true` lub `false` i zapamiętaj ich wartości jako obiekty `bool`
    6. Nie twórz dodatkowych klas do reprezentacji pola `permission`, niech zostanie jako `list[dict]`
    7. Iterując po rekordach twórz instancje tej klasy
    8. Zbierz wszystkie instancje do jednej listy
    9. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> 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']}])]
"""


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