4.5. Dataclass Postinit

  • Dataclasses generate __init__()

  • Overloading __init__() manually will destroy it

  • For init time validation there is __post_init__()

  • It is run after all parameters are set in the class

  • Hence you have to take care about negative cases (errors)

4.5.1. SetUp

>>> from dataclasses import dataclass
>>> from typing import ClassVar
>>> from datetime import date, datetime

4.5.2. Problem

  • Init serves not only for fields initialization

  • It could be also used for value validation

>>> class User:
...     firstname: str
...     lastname: str
...     age: int
...
...     def __init__(self, firstname, lastname, age):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.age = age
...         if self.age < 18:
...             raise ValueError('Invalid age')

Usage:

>>> mark = User('Mark', 'Watney', age=42)
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney', 'age': 42}
>>> mark = User('Mark', 'Watney', age=10)
Traceback (most recent call last):
ValueError: Invalid age

4.5.3. Solution

  • Creating own __init__() will overload init from dataclasses

  • Therefore in dataclasses there is __post_init__() method

  • It is run after init (as the name suggest)

  • It works on fields, which already saved (it was done in __init__)

  • No need to assign it once again

  • You can focus only on bailing-out (checking only negative path - errors)

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     age: int
...
...     def __post_init__(self):
...         if self.age < 18:
...             raise ValueError('Invalid age')

Usage:

>>> mark = User('Mark', 'Watney', age=42)
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney', 'age': 42}
>>> mark = User('Mark', 'Watney', age=10)
Traceback (most recent call last):
ValueError: Invalid age

4.5.4. Type Conversion

  • __post_init__() can also be used to convert data

  • Example str 2000-01-02 to date object date(2000, 1, 2)

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     birthdate: str | date
...
...     def __post_init__(self):
...         if isinstance(self.birthdate, str):
...             self.birthdate = date.fromisoformat(self.birthdate)
>>>
>>>
>>> User('Mark', 'Watney', birthdate='2000-01-02')
User(firstname='Mark', lastname='Watney', birthdate=datetime.date(2000, 1, 2))

4.5.5. InitVar

  • Init-only fields

  • Added as parameters to the generated __init__

  • Passed to the optional __post_init__ method

  • They are not otherwise used by Data Classes

SetUp:

>>> from dataclasses import dataclass, InitVar

Definition:

>>> @dataclass
... class User:
...     fullname: InitVar[str]
...     firstname: str | None = None
...     lastname: str | None = None
...
...     def __post_init__(self, fullname):
...         if fullname:
...             self.firstname, self.lastname = fullname.split()

Usage:

>>> mark = User('Mark Watney')
>>>
>>> mark
User(firstname='Mark', lastname='Watney')

4.5.6. Use Case - 1

>>> from dataclasses import dataclass, KW_ONLY
>>> from typing import ClassVar
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     _: KW_ONLY
...     age: int
...     AGE_MIN: ClassVar[int] = 30
...     AGE_MAX: ClassVar[int] = 50
...
...     def __post_init__(self):
...         if self.age not in range(self.AGE_MIN, self.AGE_MAX):
...             raise ValueError
>>>
>>>
>>> mark = User('Mark', 'Watney', age=42)

4.5.7. Use Case - 2

>>> from dataclasses import dataclass, InitVar
>>> from datetime import date, time, datetime, timezone
>>> from zoneinfo import ZoneInfo
>>>
>>>
>>> @dataclass
... class CurrentTime:
...     tzname: InitVar[str]
...     d: date | None = None
...     t: time | None = None
...     tz: ZoneInfo | None = None
...
...     def __post_init__(self, tzname):
...         current = datetime.now(ZoneInfo('UTC'))
...         localized = current.astimezone(ZoneInfo(tzname))
...         self.d = localized.date()
...         self.t = localized.time()
...         self.tz = localized.tzname()

Usage:

>>> now = CurrentTime('Europe/Warsaw')
>>> now  
CurrentTime(d=datetime.date(1969, 7, 21),
            t=datetime.time(2, 56, 15),
            tz='CEST')

4.5.8. Use Case - 3

>>> from dataclasses import dataclass, InitVar
>>> import datetime
>>>
>>>
>>> @dataclass
... class DateTime:
...     string: InitVar[str]
...     date: datetime.date | None = None
...     time: datetime.time | None = None
...
...     def __post_init__(self, string: str):
...         dt = datetime.datetime.fromisoformat(string)
...         self.date = dt.date()
...         self.time = dt.time()

Usage:

>>> apollo11 = DateTime('1969-07-21 02:56:15')
>>>
>>> apollo11
DateTime(date=datetime.date(1969, 7, 21), time=datetime.time(2, 56, 15))
>>>
>>> apollo11.date
datetime.date(1969, 7, 21)
>>>
>>> apollo11.time
datetime.time(2, 56, 15)

4.5.9. Use Case - 4

SetUp:

>>> from dataclasses import dataclass, field, InitVar
>>> from datetime import date
>>> @dataclass
... class Date:
...     year: InitVar[int]
...     month: InitVar[int]
...     day: InitVar[int]
...     current: date = field(default=None, init=False)
...
...     def __post_init__(self, year: int, month: int, day: int):
...         self.current = date(year, month, day)

Usage:

>>> result = Date(1969, 7, 21)
>>> vars(result)
{'current': datetime.date(1969, 7, 21)}

4.5.10. Use Case - 5

>>> from dataclasses import dataclass, InitVar
>>>
>>>
>>> @dataclass
... class Email:
...     address: InitVar[str]
...     username: str | None = None
...     domain: str | None = None
...
...     def __post_init__(self, address):
...         self.username, self.domain = address.split('@')
...
...     def get_address(self):
...         return f'{self.username}@{self.domain}'

Usage:

>>> email = Email('mwatney@nasa.gov')
>>>
>>> print(email)
Email(username='mwatney', domain='nasa.gov')
>>>
>>> print(email.username)
mwatney
>>>
>>> print(email.domain)
nasa.gov
>>>
>>> print(email.get_address())
mwatney@nasa.gov
>>>
>>> print(email.address)
Traceback (most recent call last):
AttributeError: 'Email' object has no attribute 'address'

4.5.11. Use Case - 6

>>> from dataclasses import dataclass
>>> from typing import ClassVar
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: ClassVar[int] = 30
...     AGE_MAX: ClassVar[int] = 50
...
...     def __post_init__(self):
...         min = self.AGE_MIN
...         max = self.AGE_MAX
...         if self.age not in range(min, max):
...             raise ValueError(f'Age {self.age} not in range {min} to {max}')

Usage:

>>> User('Mark', 'Watney', 60)
Traceback (most recent call last):
ValueError: Age 60 not in range 30 to 50
>>> User('Mark', 'Watney', 60, AGE_MAX=70)
Traceback (most recent call last):
TypeError: User.__init__() got an unexpected keyword argument 'AGE_MAX'

4.5.12. Use Case - 7

  • Boundary check

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Point:
...     x: int = 0
...     y: int = 0
...
...     def __post_init__(self):
...         if self.x < 0 or self.y < 0:
...             raise ValueError('Coordinate cannot be negative')

4.5.13. Use Case - 8

  • Var Range

>>> from dataclasses import dataclass, field
>>> from typing import Final
>>>
>>>
>>> @dataclass
... class Point:
...     x: int = 0
...     y: int = 0
...     X_MIN: Final[int] = 0
...     X_MAX: Final[int] = 1024
...     Y_MIN: Final[int] = 0
...     Y_MAX: Final[int] = 768
...
...     def __post_init__(self):
...         if not self.X_MIN <= self.x < self.X_MAX:
...             raise ValueError(f'x value ({self.x}) is not between {self.X_MIN} and {self.X_MAX}')
...         if not self.Y_MIN <= self.y < self.Y_MAX:
...             raise ValueError(f'y value ({self.y}) is not between {self.Y_MIN} and {self.Y_MAX}')

Usage:

>>> Point(0, 0)
Point(x=0, y=0, X_MIN=0, X_MAX=1024, Y_MIN=0, Y_MAX=768)
>>> Point(-1, 0)
Traceback (most recent call last):
ValueError: x value (-1) is not between 0 and 1024
>>> Point(0, 2000)
Traceback (most recent call last):
ValueError: y value (2000) is not between 0 and 768
>>> Point(0, 0, X_MIN=10, X_MAX=100)
Traceback (most recent call last):
ValueError: x value (0) is not between 10 and 100

4.5.14. Use Case - 9

  • Const Range

>>> from dataclasses import dataclass, field
>>> from typing import Final
>>>
>>>
>>> @dataclass
... class Point:
...     x: int = 0
...     y: int = 0
...     X_MIN: Final[int] = field(init=False, default=0)
...     X_MAX: Final[int] = field(init=False, default=1024)
...     Y_MIN: Final[int] = field(init=False, default=0)
...     Y_MAX: Final[int] = field(init=False, default=768)
...
...     def __post_init__(self):
...         if not self.X_MIN <= self.x < self.X_MAX:
...             raise ValueError(f'x value ({self.x}) is not between {self.X_MIN} and {self.X_MAX}')
...         if not self.Y_MIN <= self.y < self.Y_MAX:
...             raise ValueError(f'y value ({self.y}) is not between {self.Y_MIN} and {self.Y_MAX}')

Usage:

>>> Point(0, 0)
Point(x=0, y=0, X_MIN=0, X_MAX=1024, Y_MIN=0, Y_MAX=768)
>>> Point(0, 0, X_MIN=10, X_MAX=100)
Traceback (most recent call last):
TypeError: Point.__init__() got an unexpected keyword argument 'X_MIN'

4.5.15. Use Case - 10

  • Init, Repr

>>> from dataclasses import dataclass, field
>>> from typing import Final
>>>
>>>
>>> @dataclass
... class Point:
...     x: int = 0
...     y: int = 0
...     X_MIN: Final[int] = field(init=False, repr=False, default=0)
...     X_MAX: Final[int] = field(init=False, repr=False, default=1024)
...     Y_MIN: Final[int] = field(init=False, repr=False, default=0)
...     Y_MAX: Final[int] = field(init=False, repr=False, default=768)
...
...     def __post_init__(self):
...         if not self.X_MIN <= self.x < self.X_MAX:
...             raise ValueError(f'x value ({self.x}) is not between {self.X_MIN} and {self.X_MAX}')
...         if not self.Y_MIN <= self.y < self.Y_MAX:
...             raise ValueError(f'y value ({self.y}) is not between {self.Y_MIN} and {self.Y_MAX}')

Usage:

>>> Point(0, 0)
Point(x=0, y=0)
>>> Point(-1, 0)
Traceback (most recent call last):
ValueError: x value (-1) is not between 0 and 1024
>>> Point(0, -1)
Traceback (most recent call last):
ValueError: y value (-1) is not between 0 and 768

4.5.16. Use Case - 11

>>> from dataclasses import dataclass, InitVar
>>>
>>> @dataclass
... class Phone:
...     full_number: InitVar[str]
...
...     country_code: int = None
...     number: int = None
...
...     def __post_init__(self, full_number: str):
...         self.country_code, self.number = full_number.split(' ', maxsplit=1)

Usage:

>>> phone = Phone('+48 123 456 789')
>>> phone
Phone(country_code='+48', number='123 456 789')

4.5.17. Use Case - 12

>>> from dataclasses import dataclass, InitVar
>>> from datetime import datetime
>>>
>>>
>>> @dataclass
... class Pesel:
...     number: InitVar[str]
...
...     pesel: str = None
...     birthdate: str = None
...     gender: str = None
...     valid: bool = None
...
...     def calc_check_digit(self):
...         weights = (1, 3, 7, 9, 1, 3, 7, 9, 1, 3)
...         check = sum(w * int(n) for w, n in zip(weights, self.pesel))
...         return str((10 - check) % 10)
...
...     def __post_init__(self, number: str):
...         self.pesel = number
...         self.birthdate = datetime.strptime(number[:6], '%y%m%d').date()
...         self.gender =  'male' if int(number[-2]) % 2 else 'female'
...         self.valid = number[-1] == self.calc_check_digit()

Usage:

>>> pesel = Pesel('69072101234')
>>> pesel  
Pesel(pesel='69072101234',
      birthdate=datetime.date(1969, 7, 21),
      gender='male',
      valid=False)

4.5.18. Use Case - 13

>>> from dataclasses import dataclass, InitVar, field
>>> from datetime import date
>>> from hashlib import sha256
>>> from typing import ClassVar
>>>
>>>
>>> @dataclass(kw_only=True, slots=True)
... class User:
...     firstname: str
...     lastname: str
...     plaintext_password: InitVar[str]
...     username: str | None = None
...     password: str | None = field(default=None, repr=False, init=False)
...     age: int | None = None
...     weight: float | None = None
...     birthdate: str | date | None = None
...     AGE_MIN: ClassVar[int] = 30
...     AGE_MAX: ClassVar[int] = 50
...
...     def __post_init__(self, plaintext_password):
...         self.password = sha256(plaintext_password.encode()).hexdigest()
...         if isinstance(self.birthdate, str):
...             self.birthdate = date.fromisoformat(self.birthdate)
...         if not self.AGE_MIN <= self.age <= self.AGE_MAX:
...             raise ValueError('Invalid age')

Create an object:

>>> mark = User(
...     firstname='Mark',
...     lastname='Watney',
...     username='mwatney',
...     plaintext_password='Ares3',
...     age=40,
...     weight=56,
...     birthdate='1969-07-21',
... )
>>> mark
User(firstname='Mark', lastname='Watney', username='mwatney', age=40, weight=56, birthdate=datetime.date(1969, 7, 21))
>>> mark.password
'31a7f50d4096c10f7d95fec5f8940ed7142a5c582b38551af49b0533fa97d407'

4.5.19. Use Case - 14

>>> import re
>>>
>>> @dataclass
... class EmailValidator:
...     domain: InitVar[str]
...     pattern: re.Pattern = None
...
...     def __post_init__(self, domain: str):
...         self.pattern = re.compile(fr'^[a-z]+@{domain}$')
...
...     def is_valid(self, email: str) -> bool:
...         return bool(self.pattern.match(email))

Usage:

>>> email = EmailValidator(domain='nasa.gov')
>>>
>>> email.is_valid('mwatney@nasa.gov')
True
>>>
>>> email.is_valid('mwatney@nasa.gov.pl')
False

Rationale:

>>> email
EmailValidator(pattern=re.compile('^[a-z]+@nasa.gov$'))
>>>
>>> hasattr(email, 'domain')
False

4.5.20. Use Case - 15

>>> from dataclasses import InitVar
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     email: InitVar[str]
...     username: str | None = None
...
...     def __post_init__(self, email: str):
...         self.username = email.split('@')[0]
>>>
>>> mark = User('Mark', 'Watney', 'mwatney@nasa.gov')

4.5.21. Use Case - 16

>>> @dataclass
... class Validator:
...     regex: InitVar[str] = ''
...     pattern: re.Pattern | None = None
...
...     def __post_init__(self, regex: str):
...         self.pattern = re.compile(regex)
...
...     def match(self, string: str):
...         return self.pattern.match(string)
...
...
>>> validator = Validator(r'^[a-z]+$')
>>> validator.match('mwatney@nasa.gov')

4.5.22. Assignments

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Dataclass PostInit Syntax
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3

# %% English
# 1. Use Dataclass to define class `Point` with attributes:
#    - `x: int` with default value `0`
#    - `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:
#    - `x: int` z domyślną wartością `0`
#    - `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ść

# %% Hints
# - `raise ValueError('error message')`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isclass
>>> from dataclasses import is_dataclass

>>> assert isclass(Point)
>>> assert is_dataclass(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


# Use Dataclass to define class `Point` with attributes: `x` and `y`
# type: type
@dataclass
class Point:
    x: int = 0
    y: int = 0


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Dataclass PostInit DatabaseDump
# - Difficulty: medium
# - Lines: 5
# - Minutes: 5

# %% English
# 1. You received input data in JSON format from the API
#    - `str` fields: firstname, lastname, role, username, password, email,
#    - `date` field: birthdate,
#    - `datetime` field: last_login (optional),
#    - `bool` fields: is_active, is_staff, is_superuser,
#    - `list[dict]` field: user_permissions
# 2. Parse fields with dates and store as `date` or `datetime` objects
# 3. Use `__post_init__()`
# 4. Run doctests - all must succeed

# %% Polish
# 1. Otrzymałeś z API dane wejściowe w formacie JSON
#    - pola `str`: firstname, lastname, role, username, password, email,
#    - pole `date`: birthdate,
#    - pole `datetime`: last_login (opcjonalne),
#    - pola `bool`: is_active, is_staff, is_superuser,
#    - pola `list[dict]`: user_permissions
# 2. Sparsuj pola z datami i zapisz je jako obiekty `date` lub `datetime`
# 3. Użyj `__post_init__()`
# 4. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `birthdate: date`
# - `last_login: datetime | None`
# - `date.fromisoformat(...)`
# - `datetime.fromisoformat(...)`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 10), \
'Python 3.10+ required'

>>> from inspect import isclass
>>> from dataclasses import is_dataclass

>>> assert isclass(User)
>>> assert is_dataclass(User)

>>> attributes = User.__dataclass_fields__.keys()
>>> list(attributes)  # doctest: +NORMALIZE_WHITESPACE
['firstname', 'lastname', 'role', 'username', 'password', 'email', 'birthdate',
 'last_login', 'is_active', 'is_staff', 'is_superuser', 'user_permissions']

>>> data = json.loads(DATA)
>>> result = [User(**user['fields']) for user in data]

>>> last_login = [user['fields']['last_login'] for user in data]
>>> last_login # doctest: +NORMALIZE_WHITESPACE
['1970-01-01T00:00:00.000+00:00',
 None,
 None,
 '1970-01-01T00:00:00.000+00:00',
 None,
 None]

>>> last_login = [user.last_login for user in result]
>>> last_login  # doctest: +NORMALIZE_WHITESPACE
[datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc),
 None,
 None,
 datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc),
 None,
 None]


>>> birthdate = {user['fields']['birthdate'] for user in data}
>>> sorted(birthdate)  # doctest: +NORMALIZE_WHITESPACE
['1994-10-12',
 '1994-11-15',
 '1995-07-15',
 '1996-01-21',
 '1999-08-02',
 '2006-05-09']

>>> birthdate = {user.birthdate for user in result}
>>> sorted(birthdate)  # doctest: +NORMALIZE_WHITESPACE
[datetime.date(1994, 10, 12),
 datetime.date(1994, 11, 15),
 datetime.date(1995, 7, 15),
 datetime.date(1996, 1, 21),
 datetime.date(1999, 8, 2),
 datetime.date(2006, 5, 9)]

>>> result[0]  # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
User(firstname='Melissa',
     lastname='Lewis',
     role='commander',
     username='mlewis',
     password='pbkdf2_sha256$120000$gvEBNiCeTrYa0$5C+NiCeTrYsha1PHog...=',
     email='mlewis@nasa.gov',
     birthdate=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']}])

>>> result[1]  # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
User(firstname='Rick',
     lastname='Martinez',
     role='pilot',
     username='rmartinez',
     password='pbkdf2_sha256$120000$aXNiCeTrY$UfCJrBh/qhXohNiCeTrYH8...=',
     email='rmartinez@nasa.gov',
     birthdate=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']}])
"""

import json
from dataclasses import dataclass
from datetime import date, datetime


DATA = (
    '[{"model":"authorization.user","pk":1,"fields":{"firstname":"Meli'
    'ssa","lastname":"Lewis","role":"commander","username":"mlewis","p'
    'assword":"pbkdf2_sha256$120000$gvEBNiCeTrYa0$5C+NiCeTrYsha1PHogqv'
    'XNiCeTrY0CRSLYYAA90=","email":"mlewis@nasa.gov","birthdate":"1995-'
    '07-15","last_login":"1970-01-01T00:00:00.000+00:00","is_active":t'
    'rue,"is_staff":true,"is_superuser":false,"user_permissions":[{"ec'
    'lss":["add","modify","view"]},{"communication":["add","modify","v'
    'iew"]},{"medical":["add","modify","view"]},{"science":["add","mod'
    'ify","view"]}]}},{"model":"authorization.user","pk":2,"fields":{"'
    'firstname":"Rick","lastname":"Martinez","role":"pilot","username"'
    ':"rmartinez","password":"pbkdf2_sha256$120000$aXNiCeTrY$UfCJrBh/q'
    'hXohNiCeTrYH8nsdANiCeTrYnShs9M/c=","birthdate":"1996-01-21","last_'
    'login":null,"email":"rmartinez@nasa.gov","is_active":true,"is_sta'
    'ff":true,"is_superuser":false,"user_permissions":[{"communication'
    '":["add","view"]},{"eclss":["add","modify","view"]},{"science":["'
    'add","modify","view"]}]}},{"model":"authorization.user","pk":3,"f'
    'ields":{"firstname":"Alex","lastname":"Vogel","role":"chemist","u'
    'sername":"avogel","password":"pbkdf2_sha256$120000$eUNiCeTrYHoh$X'
    '32NiCeTrYZOWFdBcVT1l3NiCeTrY4WJVhr+cKg=","email":"avogel@esa.int"'
    ',"birthdate":"1994-11-15","last_login":null,"is_active":true,"is_s'
    'taff":true,"is_superuser":false,"user_permissions":[{"eclss":["ad'
    'd","modify","view"]},{"communication":["add","modify","view"]},{"'
    'medical":["add","modify","view"]},{"science":["add","modify","vie'
    'w"]}]}},{"model":"authorization.user","pk":4,"fields":{"firstname'
    '":"Chris","lastname":"Beck","role":"crew-medical-officer","userna'
    'me":"cbeck","password":"pbkdf2_sha256$120000$3G0RNiCeTrYlaV1$mVb6'
    '2WNiCeTrYQ9aYzTsSh74NiCeTrY2+c9/M=","email":"cbeck@nasa.gov","bir'
    'thdate":"1999-08-02","last_login":"1970-01-01T00:00:00.000+00:00",'
    '"is_active":true,"is_staff":true,"is_superuser":false,"user_permi'
    'ssions":[{"communication":["add","view"]},{"medical":["add","modi'
    'fy","view"]},{"science":["add","modify","view"]}]}},{"model":"aut'
    'horization.user","pk":5,"fields":{"firstname":"Beth","lastname":"'
    'Johanssen","role":"sysop","username":"bjohanssen","password":"pbk'
    'df2_sha256$120000$QmSNiCeTrYBv$Nt1jhVyacNiCeTrYSuKzJ//WdyjlNiCeTr'
    'YYZ3sB1r0g=","email":"bjohanssen@nasa.gov","birthdate":"2006-05-09'
    '","last_login":null,"is_active":true,"is_staff":true,"is_superuse'
    'r":false,"user_permissions":[{"communication":["add","view"]},{"s'
    'cience":["add","modify","view"]}]}},{"model":"authorization.user"'
    ',"pk":6,"fields":{"firstname":"Mark","lastname":"Watney","role":"'
    'botanist","username":"mwatney","password":"pbkdf2_sha256$120000$b'
    'xS4dNiCeTrY1n$Y8NiCeTrYRMa5bNJhTFjNiCeTrYp5swZni2RQbs=","email":"'
    'mwatney@nasa.gov","birthdate":"1994-10-12","last_login":null,"is_a'
    'ctive":true,"is_staff":true,"is_superuser":false,"user_permission'
    's":[{"communication":["add","modify","view"]},{"science":["add","'
    'modify","view"]}]}}]'
)


# Using `dataclass` model data as class `User`
# type: type
@dataclass
class User:
    firstname: str
    lastname: str
    role: str
    username: str
    password: str
    email: str
    birthdate: date
    last_login: datetime | None
    is_active: bool
    is_staff: bool
    is_superuser: bool
    user_permissions: list[dict]