Гайд по dataclass в Python

Полное руководство по модулю dataclasses: декораторы, поля, наследование, frozen, slots, валидация, сравнение, InitVar, ClassVar, сериализация, FastAPI, SQLAlchemy и best practices

Версия 2.0 • 2026

1. Введение в dataclass

dataclasses — модуль стандартной библиотеки Python, добавленный в версии 3.7 (PEP 557). Предоставляет декоратор @dataclass, который автоматически генерирует специальные методы (__init__, __repr__, __eq__, __hash__ и другие) для классов, предназначенных в основном для хранения данных.

Основная идея: вы описываете данные (поля с типами), а Python сам генерирует «скучной» код, который иначе пришлось бы писать вручную.

Зачем нужен dataclass

  • Меньше кода: не нужно писать __init__, __repr__, __eq__ вручную
  • Типизация: поля аннотированы типами — лучше читаемость, поддержка IDE и mypy
  • Неизменяемость: параметр frozen=True создаёт иммутабельный объект
  • Сравнение: автоматическая сортировка и сравнение по полям
  • Сериализация: легкое преобразование в dict/tuple для JSON, баз данных
  • Валидация: __post_init__ для проверки значений после инициализации
  • Производительность: slots=True (Python 3.10+) уменьшает память и ускоряет доступ
  • InitVar: временные переменные, передаваемые в init, но не сохраняемые как поля
  • Интроспекция: __dataclass_fields__ для программного анализа структуры

2. Базовая структура

Минимальный пример

from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str
    age: int = 0

user = User(name="Alice", email="alice@example.com", age=30)
print(user)  # User(name='Alice', email='alice@example.com', age=30)

Этот код автоматически генерирует:

  • __init__(self, name: str, email: str, age: int = 0)
  • __repr__(self) — читаемое представление
  • __eq__(self, other) — сравнение по всем полям

Что генерирует dataclass

МетодУсловиеОписание
__init__Всегда (кроме init=False)Конструктор с полями в порядке объявления
__repr__Всегда (кроме repr=False)ClassName(field=value, ...)
__eq__Всегда (кроме eq=False)Сравнение по всем полям
__hash__Зависит от frozen/eqХэш: если frozen=True или unsafe_hash=True
__lt__, __le__, __gt__, __ge__order=TrueОператоры для сортировки
__delattr__, __setattr__frozen=TrueЗапрет на изменение полей
__match_args__match_args=True (3.10+)Поддержка pattern matching

Правило генерации __hash__

# eq=True + frozen=True → __hash__ генерируется (хэшируемый)
# eq=True + frozen=False → __hash__ = None (нехэшируемый)
# eq=False → __hash__ наследуется от object
# unsafe_hash=True → __hash__ принудительно (даже для мутабельного)

@dataclass(frozen=True)
class Hashable:
    x: int

print(hash(Hashable(1)))  # OK

@dataclass
class NotHashable:
    x: int

# hash(NotHashable(1))  # TypeError: unhashable type

3. Параметры dataclass

ПараметрПо умолч.ОписаниеВерсия
initTrueГенерировать __init__3.7
reprTrueГенерировать __repr__3.7
eqTrueГенерировать __eq__3.7
orderFalseГенерировать __lt__, __le__, __gt__, __ge__3.7
frozenFalseСделать экземпляры неизменяемыми3.7
unsafe_hashFalseПринудительно генерировать __hash__3.7
match_argsTrueГенерировать __match_args__ для pattern matching3.10
kw_onlyFalseВсе поля — keyword-only3.10
slotsFalseГенерировать __slots__3.10
weakref_slotFalseДобавить __weakref__ в slots3.11
@dataclass(order=True, frozen=True, kw_only=True)
class Point:
    x: float
    y: float

p1 = Point(x=1.0, y=2.0)
p2 = Point(x=1.0, y=3.0)
print(p1 < p2)  # True (сравнение по x, потом по y)

# Все параметры False:
@dataclass(init=False, repr=False, eq=False, order=False)
class Raw:
    """Полностью ручной класс — ничего не генерируется"""
    name: str

    def __init__(self, name: str):
        self.name = name.upper()

4. Поля (field)

Функция field() позволяет задать детальные настройки для каждого поля:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Order:
    id: int
    items: List[str] = field(default_factory=list)
    discount: float = field(default=0.0, repr=False)
    created_at: str = field(compare=False)
    metadata: dict = field(default_factory=dict, hash=False, compare=False)

Параметры field

ПараметрНазначение
defaultЗначение по умолчанию
default_factoryФабрика для мутабельных дефолтов (list, dict, set, lambda)
initВключить поле в __init__ (по умолч. True)
reprВключить поле в __repr__ (по умолч. True)
compareУчитывать поле при сравнении (по умолч. True)
hashУчитывать поле при вычислении хэша (по умолч. None = как compare)
metadataПроизвольные метаданные (dict)
kw_onlyСделать поле keyword-only (3.10+)
Важно: Мутабельные дефолты (list, dict, set) обязательно через default_factory. items: List[str] = []ValueError!

Метаданные (metadata)

@dataclass
class Product:
    name: str = field(metadata={"description": "Название товара", "max_length": 255})
    price: float = field(metadata={"unit": "USD", "min": 0.0, "currency": "USD"})

print(Product.__dataclass_fields__["name"].metadata["description"])
print(Product.__dataclass_fields__["price"].metadata)
# → {'unit': 'USD', 'min': 0.0, 'currency': 'USD'}

// Пример: генерация JSON Schema из metadata
def fields_to_json_schema(cls):
    schema = {"type": "object", "properties": {}}
    for name, f in cls.__dataclass_fields__.items():
        schema["properties"][name] = f.metadata
    return schema

print(fields_to_json_schema(Product))
# {'type': 'object', 'properties': {
#   'name': {'description': 'Название товара', 'max_length': 255},
#   'price': {'unit': 'USD', 'min': 0.0, 'currency': 'USD'}
# }}

field с кастомным default_factory

import secrets, uuid
from datetime import datetime, timedelta

@dataclass
class AuthSession:
    user_id: int
    token: str = field(default_factory=lambda: secrets.token_hex(32))
    csrf_token: str = field(default_factory=lambda: secrets.token_urlsafe(32))
    expires_at: datetime = field(
        default_factory=lambda: datetime.now() + timedelta(hours=2)
    )
    id: uuid.UUID = field(default_factory=uuid.uuid4)
    metadata: dict = field(default_factory=lambda: {"version": 1})

s = AuthSession(user_id=1)
print(s.token)       # случайный 64-символьный hex
print(s.expires_at)  # +2 часа

5. Типы полей

Стандартные типы

from dataclasses import dataclass, field
from typing import Optional, List, Dict, Set, Tuple, Union, Any, Literal
from decimal import Decimal
from datetime import datetime, date, time
from uuid import UUID
from pathlib import Path
from enum import Enum, auto

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

@dataclass
class AllTypes:
    # Примитивы
    id: int
    name: str
    active: bool
    ratio: float
    data: bytes

    # Optional
    nickname: Optional[str] = None

    # Коллекции
    tags: List[str] = field(default_factory=list)
    scores: Dict[str, float] = field(default_factory=dict)
    roles: Set[str] = field(default_factory=set)
    matrix: List[List[int]] = field(default_factory=list)

    # Специализированные
    balance: Decimal = Decimal("0.00")
    created_at: datetime = field(default_factory=datetime.now)
    birth_date: date = field(default_factory=date.today)
    lunch_time: time = time(12, 0)
    uid: UUID = field(default_factory=UUID.uuid4)
    path: Path = Path("/tmp")
    color: Color = Color.RED

    # Union / Any / Literal
    payload: Any = None
    status: Literal["pending", "active", "done"] = "pending"
    metadata: Union[Dict[str, Any], None] = None

    # Tuple (фиксированная длина)
    coords: Tuple[float, float] = (0.0, 0.0)
    config: Tuple[int, str, bool] = (0, "", False)

Вложенные dataclass

@dataclass
class Address:
    city: str
    street: str
    zip_code: str
    is_primary: bool = False

@dataclass
class Employee:
    name: str
    address: Address
    department: str
    addresses: List[Address] = field(default_factory=list)
    metadata: Dict[str, Address] = field(default_factory=dict)

emp = Employee(
    name="Bob",
    address=Address(city="Москва", street="Тверская", zip_code="101000"),
    department="IT",
    addresses=[
        Address(city="Москва", street="Тверская", zip_code="101000", is_primary=True),
        Address(city="СПб", street="Невский", zip_code="190000"),
    ]
)
print(emp)
# Employee(name='Bob', address=Address(...), ...)

Рекурсивные dataclass (деревья)

from typing import Optional, List

@dataclass
class TreeNode:
    value: int
    left: Optional["TreeNode"] = None
    right: Optional["TreeNode"] = None

    def is_leaf(self) -> bool:
        return self.left is None and self.right is None

tree = TreeNode(
    value=1,
    left=TreeNode(
        value=2,
        left=TreeNode(4),
        right=TreeNode(5),
    ),
    right=TreeNode(value=3),
)

@dataclass
class Comment:
    id: int
    text: str
    replies: List["Comment"] = field(default_factory=list)
    parent: Optional["Comment"] = None

    def add_reply(self, text: str) -> "Comment":
        reply = Comment(id=len(self.replies) + 1, text=text, parent=self)
        self.replies.append(reply)
        return reply

post = Comment(id=1, text="Great post!")
post.add_reply("Thanks!")
post.add_reply("I agree")

6. post_init — валидация

Метод __post_init__ выполняется сразу после __init__. Используется для валидации, вычисления полей, преобразования типов.

Валидация значений

@dataclass
class Person:
    name: str
    age: int
    email: str = ""

    def __post_init__(self):
        if self.age < 0:
            raise ValueError(f"Age must be positive: {self.age}")
        if not self.name.strip():
            raise ValueError("Name cannot be empty")
        self.name = self.name.strip().title()
        if self.email and "@" not in self.email:
            raise ValueError(f"Invalid email: {self.email}")

p = Person(name="  alice  ", age=30)
print(repr(p))  # Person(name='Alice', age=30, email='')

Вычисляемые поля

from datetime import datetime
import hashlib

@dataclass
class Article:
    title: str
    content: str
    created_at: datetime = field(default_factory=datetime.now, init=False)
    slug: str = field(init=False)
    content_hash: str = field(init=False, repr=False)

    def __post_init__(self):
        self.slug = self.title.lower().replace(" ", "-")
        import re
        self.slug = re.sub(r"[^a-z0-9-]", "", self.slug)
        self.content_hash = hashlib.md5(self.content.encode()).hexdigest()

a = Article(title="Hello World!", content="Some text")
print(a.slug)          # "hello-world"
print(a.created_at)    # 2026-06-20 12:34:56
print(a.content_hash)  # md5 хэш контента

Преобразование типов в post_init

from datetime import datetime
from decimal import Decimal
from uuid import UUID

@dataclass
class ParsedEvent:
    raw_id: str
    raw_date: str
    raw_amount: str

    id: UUID = field(init=False)
    date: datetime = field(init=False)
    amount: Decimal = field(init=False)

    def __post_init__(self):
        self.id = UUID(self.raw_id)
        self.date = datetime.fromisoformat(self.raw_date)
        self.amount = Decimal(self.raw_amount)

event = ParsedEvent(
    raw_id="550e8400-e29b-41d4-a716-446655440000",
    raw_date="2026-06-20T12:00:00",
    raw_amount="199.99",
)
print(event.id)     # UUID(...)
print(event.amount) # Decimal('199.99')

Порядок вызова post_init при наследовании

@dataclass
class BaseConfig:
    name: str

    def __post_init__(self):
        print(f"BaseConfig: name={self.name}")
        self._validate()

    def _validate(self):
        if not self.name:
            raise ValueError("Name required")

@dataclass
class ExtendedConfig(BaseConfig):
    version: int = 1

    def __post_init__(self):
        super().__post_init__()  # обязательно!
        print(f"ExtendedConfig: version={self.version}")
        if self.version < 0:
            raise ValueError("Version must be positive")

c = ExtendedConfig(name="app", version=2)
# BaseConfig: name=app
# ExtendedConfig: version=2

7. Наследование

Простое наследование

@dataclass
class BaseUser:
    id: int
    name: str
    created_at: datetime = field(default_factory=datetime.now, init=False)

@dataclass
class Admin(BaseUser):
    level: int = 1
    permissions: List[str] = field(default_factory=lambda: ["read"])

    def grant(self, perm: str) -> None:
        self.permissions.append(perm)

admin = Admin(id=1, name="Alice", level=5)
admin.grant("write")
print(admin)
# Admin(id=1, name='Alice', level=5, permissions=['read', 'write'],
#        created_at=datetime(...))

Проблема порядка полей

Поля родителя без умолчания идут первыми в __init__. Если в дочернем классе есть поля с умолчанием, они не могут стоять перед полями родителя — TypeError.
@dataclass
class Base:
    name: str  # обязательное

# Ошибка!
# @dataclass
# class Child(Base):
#     extra: str = "default"
# # TypeError: non-default argument follows default argument
# # (name идёт после extra в сгенерированном __init__)

# Решение 1: kw_only на уровне класса
@dataclass(kw_only=True)
class Animal:
    name: str
    species: str

@dataclass(kw_only=True)
class Dog(Animal):
    breed: str = "unknown"
    age: int = 0

Dog(name="Rex", species="Canine", breed="Husky")  # OK

# Решение 2: дать умолчание полю родителя
@dataclass
class Base2:
    name: str = "default"

@dataclass
class Child2(Base2):
    extra: str = "value"

Child2("test", "extra")  # OK

Множественное наследование

@dataclass
class Named:
    name: str

@dataclass
class Versioned:
    version: int = 1

@dataclass
class Timestamped:
    created_at: datetime = field(default_factory=datetime.now, init=False)

@dataclass
class FullEntity(Named, Versioned, Timestamped):
    id: int

e = FullEntity(name="test", id=1, version=2)
print(e.created_at)  # datetime.now()

# MRO: FullEntity → Named → Versioned → Timestamped → object
# Поля: name, version, id, created_at (init=False)

8. Frozen dataclass

frozen=True делает экземпляры неизменяемыми. Попытка изменить поле — FrozenInstanceError.

Базовый пример

@dataclass(frozen=True)
class Config:
    host: str
    port: int
    debug: bool = False

cfg = Config(host="localhost", port=8080)
# cfg.port = 9090  # FrozenInstanceError

# Frozen → хэшируемый → можно в set/dict keys
configs: set = {Config("a", 1), Config("b", 2), Config("a", 1)}
print(len(configs))  # 2

replace — копирование с изменениями

from dataclasses import replace

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = replace(p1, x=10)

print(p1)  # Point(x=1, y=2) — исходный не изменился
print(p2)  # Point(x=10, y=2) — новый объект

// replace в цепочках:
p3 = replace(p2, y=20)
print(p3)  # Point(x=10, y=20)

// replace с вычисляемым значением:
p4 = replace(p1, x=p1.x + 5)
print(p4)  # Point(x=6, y=2)

Frozen + post_init

В __post_init__ frozen-класса нельзя присвоить поле напрямую. Используйте object.__setattr__:

@dataclass(frozen=True)
class ImmutablePerson:
    name: str
    age: int
    age_group: str = field(init=False)

    def __post_init__(self):
        object.__setattr__(self, "age_group",
            "child" if self.age < 18 else
            "teen" if self.age < 21 else
            "adult")

p = ImmutablePerson(name="Alice", age=25)
print(p.age_group)  # "adult"

// Утилита для удобства:
def set_frozen(obj, **kwargs):
    for k, v in kwargs.items():
        object.__setattr__(obj, k, v)

@dataclass(frozen=True)
class FrozenConfig:
    host: str

    def __post_init__(self):
        set_frozen(self, url=f"https://{self.host}")

print(FrozenConfig(host="example.com").url)
# https://example.com

Frozen как value object

@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str = "USD"

    def __add__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("Currency mismatch")
        return Money(self.amount + other.amount, self.currency)

    def __mul__(self, factor: float) -> "Money":
        return Money(self.amount * Decimal(str(factor)), self.currency)

    def __repr__(self):
        return f"{self.amount:.2f} {self.currency}"

price = Money(Decimal("99.99"))
tax = Money(Decimal("10.00"))
total = price + tax
bulk = price * 3
print(total)  # 109.99 USD
print(bulk)   # 299.97 USD

9. Сравнение и сортировка

Порядок сравнения

@dataclass(order=True)
class Task:
    priority: int
    created_at: str
    title: str = field(compare=False)

tasks = [
    Task(2, "2026-01-01", "Fix bug"),
    Task(1, "2026-01-02", "Release"),
    Task(2, "2026-01-03", "Refactor"),
    Task(1, "2026-01-01", "Deploy"),
]

for t in sorted(tasks):
    print(f"P{t.priority} | {t.created_at} | {t.title}")
# P1 | 2026-01-01 | Deploy    (priority 1, ранняя дата)
# P1 | 2026-01-02 | Release   (priority 1, поздняя дата)
# P2 | 2026-01-01 | Fix bug   (priority 2, ранняя дата)
# P2 | 2026-01-03 | Refactor  (priority 2, поздняя дата)

Кастомный порядок сортировки

@dataclass(order=True)
class Score:
    sort_key: int = field(init=False, repr=False, compare=True)
    name: str
    value: int

    def __post_init__(self):
        self.sort_key = -self.value  # обратный порядок (высший балл — первым)

scores = [
    Score("Bob", 85),
    Score("Alice", 92),
    Score("Charlie", 78),
]

for s in sorted(scores):
    print(f"{s.name}: {s.value}")
# Alice: 92  (высший балл)
# Bob: 85
# Charlie: 78

// Сортировка по нескольким полям:
@dataclass(order=True)
class Employee:
    department_sort: int = field(init=False, repr=False, compare=True)
    salary_sort: int = field(init=False, repr=False, compare=True)
    name: str
    department: str
    salary: int

    def __post_init__(self):
        dept_order = {"IT": 0, "HR": 1, "Sales": 2}
        self.department_sort = dept_order.get(self.department, 99)
        self.salary_sort = -self.salary  # высокая зарплата — первая

10. Преобразование в dict / tuple / JSON

asdict — в словарь (рекурсивно)

from dataclasses import dataclass, asdict, astuple
import json

@dataclass
class Address:
    city: str
    zip_code: str

@dataclass
class Person:
    name: str
    age: int
    address: Address
    tags: List[str] = field(default_factory=list)

p = Person("Alice", 30, Address("Москва", "101000"), tags=["admin"])
data = asdict(p)

print(data)
# {'name': 'Alice', 'age': 30,
#  'address': {'city': 'Москва', 'zip_code': '101000'},
#  'tags': ['admin']}

json.dumps(asdict(p), ensure_ascii=False, indent=2)
# Полноценный JSON с русскими буквами

astuple — в кортеж

t = astuple(p)
print(t)  # ('Alice', 30, ('Москва', '101000'), ['admin'])

// Полезно для тестов:
assert astuple(p) == ("Alice", 30, ("Москва", "101000"), ["admin"])

// Распаковка как кортеж:
name, age, addr, tags = astuple(p)

Кастомная сериализация в JSON

from datetime import datetime
from uuid import UUID
from decimal import Decimal

class DataclassJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, UUID):
            return str(obj)
        if isinstance(obj, datetime):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, set):
            return list(obj)
        if isinstance(obj, bytes):
            return obj.hex()
        return super().default(obj)

@dataclass
class Order:
    id: UUID
    created_at: datetime
    total: Decimal
    tags: Set[str]
    data: bytes

def to_json(obj) -> str:
    return json.dumps(asdict(obj), cls=DataclassJSONEncoder, indent=2)

order = Order(
    id=UUID("550e8400-e29b-41d4-a716-446655440000"),
    created_at=datetime.now(),
    total=Decimal("199.99"),
    tags={"urgent", "premium"},
    data=b"\x00\x01\x02",
)
print(to_json(order))
# {
#   "id": "550e8400-e29b-41d4-a716-446655440000",
#   "created_at": "2026-06-20T12:34:56.789000",
#   "total": 199.99,
#   "tags": ["premium", "urgent"],
#   "data": "000102"
# }

Десериализация из dict (ручная)

from dataclasses import dataclass
from typing import List, Optional

@dataclass
class Tag:
    name: str
    color: str = "blue"

@dataclass
class Article:
    title: str
    tags: List[Tag]
    author: Optional[str] = None

# Ручная десериализация с вложенными объектами:
def article_from_dict(data: dict) -> Article:
    tags = [Tag(**t) if isinstance(t, dict) else t for t in data["tags"]]
    return Article(
        title=data["title"],
        tags=tags,
        author=data.get("author"),
    )

raw = {
    "title": "My Article",
    "tags": [{"name": "python", "color": "blue"}, {"name": "dataclass"}],
}
article = article_from_dict(raw)
print(article)
# Article(title='My Article', tags=[Tag(name='python', color='blue'),
#        Tag(name='dataclass', color='blue')], author=None)

11. Сравнение с альтернативами

ХарактеристикаdataclassnamedtupleTypedDictattrspydantic
Стандартная библиотека✅ 3.7+✅ 2.6+✅ 3.8+
Мутабельность✅ / frozen✅ (нотация)✅ / frozen
Аннотации типов❌ (строки)✅ строгая
ВалидацияРучная post_init✅ @validator✅ Field, model_validator
Сериализацияasdict, astuple_asdictdict()asdict, serializemodel_dump, model_dump_json
ДесериализацияРучнаяРучнаяРучная@define + convertermodel_validate
JSON Schema❌ (есть add-ons)✅ model_json_schema
Слоты✅ slots=True (3.10+)✅ slots=True
Generic✅ Generic[T]✅ BaseModel + Generic
ПроизводительностьВысокаяВысокаяВысокаяСредняяНиже (валидация)
Вес зависимости0 KB0 KB0 KB~200 KB~2 MB

Когда что выбрать

  • dataclass — 90% случаев: DTO, конфиги, модели без сложной валидации
  • namedtuple — лёгкие иммутабельные структуры, совместимость с tuple
  • TypedDict — типизация обычных dict'ов (kwargs, конфиги)
  • attrs — когда нужно больше, чем dataclass (валидаторы, конвертеры) и нельзя pydantic
  • pydantic — строгая валидация, JSON Schema, FastAPI, внешние данные

12. init=False и KW_ONLY

init=False — поле не в init

import hashlib

@dataclass
class User:
    username: str
    password_hash: str = field(init=False)

    def __post_init__(self):
        self.password_hash = hashlib.sha256(
            self.username.encode()
        ).hexdigest()

u = User(username="alice")
print(u.password_hash)  # вычислен автоматически

KW_ONLY (Python 3.10+)

Маркер KW_ONLY разделяет поля: слева — позиционные, справа — keyword-only.

from dataclasses import dataclass, KW_ONLY

@dataclass
class Product:
    name: str          # позиционное
    price: float       # позиционное
    _: KW_ONLY
    category: str = "general"   # keyword-only
    discount: float = 0.0       # keyword-only
    warehouse: str = "main"     # keyword-only

p1 = Product("Laptop", 999.99, category="electronics")
p2 = Product("Mouse", 25.0)
# p3 = Product("Laptop", 999.99, "electronics")  # TypeError!

// Позиционные — обязательные, keyword-only — опциональные с умолчаниями

kw_only на уровне класса vs KW_ONLY

// kw_only=True — все поля keyword-only
@dataclass(kw_only=True)
class Person:
    name: str
    age: int

Person(name="Alice", age=30)  # OK
# Person("Alice", 30)  # TypeError

// KW_ONLY — только часть полей keyword-only
@dataclass
class Order:
    id: int
    items: List[str]
    _: KW_ONLY
    coupon: str = ""
    priority: str = "normal"

// Комбинированный пример:
@dataclass
class APIRequest:
    method: str                    # позиционное
    path: str                      # позиционное
    _: KW_ONLY
    headers: Dict[str, str] = field(default_factory=dict)  # keyword-only
    body: Any = None                                        # keyword-only
    timeout: int = 30                                       # keyword-only

req = APIRequest("GET", "/users", headers={"Auth": "Bearer x"}, timeout=10)

13. Слоты (slots)

slots=True (3.10+) генерирует класс с __slots__ — меньше памяти, быстрее доступ к полям.

Сравнение памяти

import sys

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

@dataclass(slots=True)
class Slotted:
    x: int
    y: int
    z: int

r = Regular(1, 2, 3)
s = Slotted(1, 2, 3)

print(r.__dict__)  # {'x': 1, 'y': 2, 'z': 3}
# print(s.__dict__)  # AttributeError: 'Slotted' object has no attribute '__dict__'

print(sys.getsizeof(r))  # 48 bytes + __dict__ (≈ 200+ bytes всего)
print(sys.getsizeof(s))  # 32 bytes (только значения в слотах)

// Массовое создание (100000 экземпляров):
# Regular: ~ 250 MB
# Slotted: ~ 56 MB

Ограничения slots

# 1. Нельзя добавить новое поле динамически
s = Slotted(1, 2, 3)
# s.new_attr = 5  # AttributeError

# 2. Наследование: __dict__ появляется, если родитель без slots
@dataclass
class Base:
    x: int

@dataclass(slots=True)
class Child(Base):
    y: int

c = Child(1, 2)
print(hasattr(c, '__dict__'))  # True! (из-за Base)

# 3. weakref — нужно добавлять явно
@dataclass(slots=True, weakref_slot=True)  # 3.11+
class Weakable:
    x: int

# 4. Нет множественного наследования со slots
@dataclass(slots=True)
class A: x: int
@dataclass(slots=True)
class B: y: int
# class C(A, B): pass  # TypeError: multiple bases have instance lay-out conflict

14. Дженерики (Generic dataclass)

Generic dataclass

from typing import Generic, TypeVar, List, Dict, Optional

T = TypeVar("T")
V = TypeVar("V")
K = TypeVar("K")

@dataclass
class Pair(Generic[T, V]):
    first: T
    second: V

@dataclass
class Response(Generic[T]):
    data: T
    status: int
    message: str = ""

@dataclass
class Paginated(Generic[T]):
    items: List[T]
    total: int
    page: int
    page_size: int

# Использование:
pair = Pair[int, str](1, "hello")
resp = Response[List[int]]([1, 2, 3], 200)
page = Paginated[str](["a", "b"], total=2, page=1, page_size=10)

reveal_type(pair.first)    # int
reveal_type(resp.data[0])  # int
reveal_type(page.items)    # List[str]

Generic с ограничениями

# TypeVar с bound
N = TypeVar("N", int, float)
Comparable = TypeVar("Comparable", bound="ComparableProtocol")

@dataclass
class Range(Generic[N]):
    min: N
    max: N

    def contains(self, value: N) -> bool:
        return self.min <= value <= self.max

r = Range[int](1, 10)
print(r.contains(5))  # True
# Range[str]("a", "z")  # mypy ошибка (str не int|float)

// Generic + default_factory
@dataclass
class Stack(Generic[T]):
    items: List[T] = field(default_factory=list)

    def push(self, item: T) -> None:
        self.items.append(item)

    def pop(self) -> Optional[T]:
        return self.items.pop() if self.items else None

s = Stack[int]()
s.push(1)
s.push(2)
print(s.pop())  # 2

Generic + наследование

@dataclass
class IntPair(Pair[int, int]):
    """Pair[int, int] с методом sum"""

    def sum(self) -> int:
        return self.first + self.second

    def product(self) -> int:
        return self.first * self.second

ip = IntPair(3, 4)
print(ip.sum())     # 7
print(ip.product()) # 12

// Generic с несколькими TypeVar:
S = TypeVar("S")
T = TypeVar("T")

@dataclass
class BiMap(Generic[S, T]):
    forward: Dict[S, T] = field(default_factory=dict)
    backward: Dict[T, S] = field(default_factory=dict)

    def add(self, key: S, value: T) -> None:
        self.forward[key] = value
        self.backward[value] = key

    def get(self, key: S) -> Optional[T]:
        return self.forward.get(key)

    def inverse(self, value: T) -> Optional[S]:
        return self.backward.get(value)

15. InitVar

InitVar — переменная, передаваемая в __init__, но не становящаяся полем экземпляра. Доступна только в __post_init__. Идеально для «временных» параметров конструктора.

Базовый пример

from dataclasses import dataclass, InitVar, field
from typing import Optional

@dataclass
class DatabaseConnection:
    host: str
    port: int
    db_url: str = field(init=False)

    # InitVar — передаётся в init, но не сохраняется
    password: InitVar[Optional[str]] = None

    def __post_init__(self, password: Optional[str]):
        # password доступен ТОЛЬКО здесь
        if password:
            self.db_url = f"postgresql://user:{password}@{self.host}:{self.port}/db"
        else:
            self.db_url = f"postgresql://@{self.host}:{self.port}/db"

conn = DatabaseConnection("localhost", 5432, password="secret")
print(conn.db_url)  # postgresql://user:secret@localhost:5432/db
# print(conn.password)  # AttributeError: 'DatabaseConnection' object has no attribute 'password'

Пример: dependency injection через InitVar

@dataclass
class Service:
    repo: "Repository" = field(init=False)
    logger: "Logger" = field(init=False)

    # Внешние зависимости — InitVar
    db_session: InitVar["Session"]
    log_level: InitVar[str] = "INFO"

    def __post_init__(self, db_session, log_level):
        self.repo = Repository(db_session)
        self.logger = Logger(level=log_level)

# Использование:
session = create_session()
service = Service(db_session=session, log_level="DEBUG")
# service.db_session — недоступен, session живёт только в post_init

InitVar с наследованием

@dataclass
class Base:
    name: str
    config: InitVar[dict] = field(default_factory=dict)

    def __post_init__(self, config: dict):
        print(f"Base: config keys = {list(config.keys())}")

@dataclass
class Child(Base):
    extra: str = ""
    # При наследовании InitVar ДОЛЖЕН быть передан
    # и проброшен в super().__post_init__

    def __post_init__(self, config: dict):
        super().__post_init__(config)  # проброс обязателен!
        print(f"Child: extra = {self.extra}")

c = Child(name="test", config={"key": "val"}, extra="data")
# Base: config keys = ['key']
# Child: extra = data

InitVar для тестов

@dataclass
class PaymentProcessor:
    gateway: str = "stripe"
    api_key: InitVar[str] = ""

    _client: Any = field(init=False)

    def __post_init__(self, api_key: str):
        if not api_key:
            api_key = os.environ.get("PAYMENT_API_KEY", "")
        self._client = self._create_client(api_key)

    def _create_client(self, api_key: str):
        # В тестах — заглушка
        return {"api_key": api_key, "gateway": self.gateway}

// В тестах:
@pytest.fixture
def processor():
    return PaymentProcessor(api_key="test_key_123")

// В проде:
# processor = PaymentProcessor()  # api_key из переменной окружения

16. ClassVar

ClassVar[T] — аннотация, помечающая поле как классовое (статическое), а не поле экземпляра. Dataclass полностью игнорирует такие поля — они не попадают в __init__, __repr__, __eq__.

Базовый пример

from dataclasses import dataclass, field
from typing import ClassVar, List

@dataclass
class Employee:
    name: str
    salary: float

    # ClassVar — не поле, не в init/repr/eq
    company: ClassVar[str] = "ACME Inc."
    employee_count: ClassVar[int] = 0
    all_employees: ClassVar[List["Employee"]] = []

    def __post_init__(self):
        Employee.employee_count += 1
        Employee.all_employees.append(self)

e1 = Employee("Alice", 100_000)
e2 = Employee("Bob", 80_000)
print(e1)  # Employee(name='Alice', salary=100000.0) — нет company!
print(Employee.company)          # "ACME Inc." — через класс
print(Employee.employee_count)   # 2
print(Employee.all_employees)    # [e1, e2]

# Изменение ClassVar влияет на ВСЕ экземпляры:
Employee.company = "New Corp"
print(e2.company)  # "New Corp"

ClassVar для конфигурации

@dataclass
class APIClient:
    base_url: str
    timeout: int = 30

    # Глобальные настройки для всех экземпляров
    default_headers: ClassVar[Dict[str, str]] = {
        "User-Agent": "MyApp/1.0",
        "Accept": "application/json",
    }
    retry_count: ClassVar[int] = 3
    _instance_count: ClassVar[int] = 0

    def __post_init__(self):
        APIClient._instance_count += 1

    @classmethod
    def set_global_timeout(cls, timeout: int) -> None:
        cls.default_timeout = timeout

client1 = APIClient("https://api.example.com")
client2 = APIClient("https://api2.example.com", timeout=10)

print(APIClient.default_headers)  # общие заголовки
print(APIClient._instance_count)  # 2

ClassVar vs field(init=False)

@dataclass
class Example:
    # ClassVar — не поле dataclass, чисто классовое
    class_var: ClassVar[int] = 42

    # init=False — поле экземпляра, но не в init
    instance_field: int = field(init=False, default=0)

e = Example()
print(e.class_var)        # 42 — доступ через экземпляр (унаследовано)
print(Example.class_var)  # 42 — доступ через класс
print(e.instance_field)   # 0 — есть у экземпляра

# ClassVar можно менять только через класс:
Example.class_var = 100
print(e.class_var)  # 100

# instance_field уникален для каждого экземпляра:
e2 = Example()
e2.instance_field = 5
print(e.instance_field)   # 0
print(e2.instance_field)  # 5

17. __dataclass_fields__ — интроспекция

__dataclass_fields__ — словарь всех полей dataclass. Ключ — имя поля, значение — объект Field с метаданными.

Структура Field

@dataclass
class Config:
    host: str = field(metadata={"env": "HOST"})
    port: int = field(default=8080, metadata={"env": "PORT", "min": 1})
    debug: bool = field(default=False, repr=False)

field_map = Config.__dataclass_fields__

for name, f in field_map.items():
    print(f"{name}:")
    print(f"  type={f.type}")        #  /  / 
    print(f"  default={f.default}")   # MISSING / 8080 / False
    print(f"  init={f.init}")         # True / True / True
    print(f"  repr={f.repr}")         # True / True / False
    print(f"  compare={f.compare}")   # True / True / True
    print(f"  hash={f.hash}")         # None / None / None
    print(f"  metadata={f.metadata}") # {'env': 'HOST'} / ...

# Output:
# host:  type=, metadata={'env': 'HOST'}
# port:  type=, default=8080, metadata={'env': 'PORT', 'min': 1}
# debug: type=, default=False, repr=False

Практические применения

// 1. Генерация JSON Schema из метаданных
def to_json_schema(cls) -> dict:
    schema = {"type": "object", "properties": {}}
    for name, f in cls.__dataclass_fields__.items():
        if f.init:  # только init-поля
            prop = {}
            if f.metadata:
                prop.update(f.metadata)
            if f.default is not dataclasses.MISSING:
                prop["default"] = f.default
            schema["properties"][name] = prop
    return schema

print(to_json_schema(Config))
# {'type': 'object', 'properties': {
#   'host': {'env': 'HOST'},
#   'port': {'default': 8080, 'env': 'PORT', 'min': 1},
#   'debug': {'default': False}
# }}

// 2. Загрузка из env / dict с авто-маппингом
def from_env(cls) -> object:
    import os
    kwargs = {}
    for name, f in cls.__dataclass_fields__.items():
        env_name = f.metadata.get("env", name.upper())
        if env_name in os.environ:
            val = os.environ[env_name]
            # Простейшее приведение типов
            if f.type is int:
                val = int(val)
            elif f.type is bool:
                val = val.lower() in ("true", "1", "yes")
            kwargs[name] = val
    return cls(**kwargs)

config = from_env(Config)  # читает HOST, PORT, DEBUG из os.environ

// 3. Создание dataclass из dict с игнорированием лишних ключей
def from_dict(cls, data: dict):
    field_names = set(cls.__dataclass_fields__.keys())
    filtered = {k: v for k, v in data.items() if k in field_names}
    return cls(**filtered)

raw = {"host": "localhost", "port": 8080, "unknown": "ignored"}
config = from_dict(Config, raw)

18. Сериализация: cattrs / dataclasses-json / mashumaro

Для автоматической десериализации (dict → dataclass, JSON → dataclass) используйте специализированные библиотеки.

cattrs

Лёгкая, быстрая, кастомизируемая:

# pip install cattrs
from cattrs import structure, unstructure
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime

@dataclass
class Tag:
    name: str
    color: str = "blue"

@dataclass
class Post:
    id: int
    title: str
    tags: List[Tag]
    created_at: datetime
    author: Optional[str] = None

# Сериализация (dataclass → dict):
data = unstructure(Post(
    id=1,
    title="Hello",
    tags=[Tag("python"), Tag("web", "green")],
    created_at=datetime(2026, 6, 20, 12, 0),
))
# {'id': 1, 'title': 'Hello', 'tags': [{'name': 'python', 'color': 'blue'},
#  {'name': 'web', 'color': 'green'}], 'created_at': datetime(2026, 6, 20, 12, 0)}

# Десериализация (dict → dataclass):
raw = {
    "id": 1,
    "title": "Hello",
    "tags": [{"name": "python"}, {"name": "web", "color": "green"}],
    "created_at": "2026-06-20T12:00:00",
    "author": "Alice",
}
post = structure(raw, Post)
# Post(id=1, title='Hello', tags=[Tag(name='python', color='blue'),
#  Tag(name='web', color='green')], created_at=datetime(2026, 6, 20, 12, 0),
#  author='Alice')

// Кастомные преобразователи:
from cattrs import Converter
from uuid import UUID

conv = Converter()
conv.register_unstructure_hook(UUID, str)
conv.register_structure_hook(UUID, lambda v, _: UUID(v))

@dataclass
class Event:
    id: UUID
    name: str

event = structure({"id": "550e8400-e29b-41d4-a716-446655440000", "name": "test"}, Event)
print(event)  # Event(id=UUID(...), name='test')

dataclasses-json

Декоратор, добавляющий from_json / to_json прямо на класс:

# pip install dataclasses-json
from dataclasses_json import dataclass_json
from dataclasses import dataclass
from typing import List, Optional

@dataclass_json
@dataclass
class User:
    id: int
    name: str
    email: str
    roles: List[str] = field(default_factory=list)

# Сериализация:
user = User(id=1, name="Alice", email="a@b.com", roles=["admin"])
json_str = user.to_json()  # {"id": 1, "name": "Alice", ...}
data = user.to_dict()      # {"id": 1, "name": "Alice", ...}

# Десериализация:
user2 = User.from_json('{"id": 2, "name": "Bob", "email": "b@c.com"}')
user3 = User.from_dict({"id": 3, "name": "Charlie", "email": "c@d.com"})

// Настройка имён полей (camelCase → snake_case):
from dataclasses_json import config

@dataclass_json
@dataclass
class ApiResponse:
    created_at: str = field(metadata=config(field_name="createdAt"))
    updated_at: str = field(metadata=config(field_name="updatedAt"))

resp = ApiResponse.from_json('{"createdAt": "2026-01-01", "updatedAt": "2026-06-20"}')
print(resp)  # ApiResponse(created_at='2026-01-01', updated_at='2026-06-20')

mashumaro

Самая быстрая, строгая типизация, поддержка Union, Optional, Generic:

# pip install mashumaro
from mashumaro import DataClassDictMixin
from dataclasses import dataclass
from typing import Optional, List, Union

@dataclass
class Config(DataClassDictMixin):
    host: str
    port: int
    debug: bool = False
    tags: Optional[List[str]] = None
    metadata: Union[str, dict, None] = None

config = Config.from_dict({
    "host": "localhost",
    "port": 8080,
    "tags": ["dev", "test"],
})
print(config.to_json())  # {"host": "localhost", "port": 8080, ...}

// Валидация Union типов:
@dataclass
class Result(DataClassDictMixin):
    status: str
    data: Union[List[int], dict, None] = None

r1 = Result.from_dict({"status": "ok", "data": [1, 2, 3]})
r2 = Result.from_dict({"status": "ok", "data": {"key": "val"}})
r3 = Result.from_dict({"status": "error"})

Сравнение библиотек

БиблиотекаСкоростьВесGenericUnionКастомизацияJSON Schema
cattrsВысокая~200 KBВысокая (Converter)
dataclasses-jsonСредняя~100 KB❌ (ограничено)Средняя (metadata)
mashumaroОчень высокая~150 KBВысокая (builder)
pydanticНиже~2 MBВысокая (Field, validator)

19. Dataclass + FastAPI

FastAPI поддерживает dataclass как request/response модели (начиная с версии 0.95). Хотя pydantic — основной инструмент, dataclass можно использовать через Body(), Query() и другие зависимоcти.

Request body (POST/PUT)

from fastapi import FastAPI, Body, Query, Path, Depends
from dataclasses import dataclass, field
from typing import Optional, List
from datetime import datetime
from uuid import UUID, uuid4

app = FastAPI()

@dataclass
class CreateUserRequest:
    name: str
    email: str
    age: Optional[int] = None
    tags: List[str] = field(default_factory=list)

@dataclass
class UserResponse:
    id: UUID
    name: str
    email: str
    age: Optional[int] = None
    created_at: datetime
    tags: List[str] = field(default_factory=list)

users_db: dict = {}

@app.post("/users", response_model=UserResponse)
def create_user(data: CreateUserRequest):
    user = UserResponse(
        id=uuid4(),
        name=data.name,
        email=data.email,
        age=data.age,
        created_at=datetime.now(),
        tags=data.tags,
    )
    users_db[user.id] = user
    return user

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: UUID):
    if user_id not in users_db:
        from fastapi import HTTPException
        raise HTTPException(404, "User not found")
    return users_db[user_id]

Query и Path параметры

@dataclass
class Pagination:
    page: int = Query(1, ge=1)
    page_size: int = Query(10, ge=1, le=100)
    sort_by: str = Query("created_at")
    order: str = Query("desc", pattern="^(asc|desc)$")

@dataclass
class DateRange:
    from_date: Optional[str] = Query(None, alias="from")
    to_date: Optional[str] = Query(None, alias="to")

@app.get("/users")
def list_users(
    pagination: Pagination = Depends(),
    dates: DateRange = Depends(),
):
    return {
        "page": pagination.page,
        "page_size": pagination.page_size,
        "from": dates.from_date,
        "to": dates.to_date,
    }

// Запрос: GET /users?page=2&page_size=20&from=2026-01-01
// Ответ: {"page": 2, "page_size": 20, "from": "2026-01-01", "to": null}

Response с фильтрацией полей

@dataclass
class UserPublic:
    id: UUID
    name: str

@dataclass
class UserAdmin(UserPublic):
    email: str
    role: str
    is_active: bool
    last_login: Optional[datetime] = None

@app.get("/users/{user_id}/public", response_model=UserPublic)
def get_public_user(user_id: UUID):
    user = users_db[user_id]
    return UserPublic(id=user.id, name=user.name)

@app.get("/users/{user_id}/admin", response_model=UserAdmin)
def get_admin_user(user_id: UUID):
    user = users_db[user_id]
    return UserAdmin(
        id=user.id, name=user.name, email=user.email,
        role="admin", is_active=True, last_login=datetime.now(),
    )

20. Dataclass + SQLAlchemy

SQLAlchemy поддерживает dataclass как модели через аннотированный декларативный стиль (2.0+) и MappedAsDataclass.

MappedAsDataclass (SQLAlchemy 2.0+)

# pip install sqlalchemy
from sqlalchemy import create_engine, String, Integer, Float, DateTime, ForeignKey, select
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
from sqlalchemy.orm import relationship
from datetime import datetime
from typing import List, Optional
from decimal import Decimal

class Base(DeclarativeBase, MappedAsDataclass):
    """Базовый класс: SQLAlchemy + dataclass"""
    pass

@dataclass
class Product(Base):
    __tablename__ = "products"

    id: Mapped[int] = mapped_column(primary_key=True, init=False)
    name: Mapped[str] = mapped_column(String(255))
    price: Mapped[Decimal] = mapped_column(Float)  # Decimal → Float в БД
    created_at: Mapped[datetime] = mapped_column(
        DateTime, default_factory=datetime.now, init=False
    )
    category_id: Mapped[Optional[int]] = mapped_column(
        ForeignKey("categories.id"), default=None
    )

    # relationship — не поле dataclass
    category: Mapped[Optional["Category"]] = relationship(default=None)

@dataclass
class Category(Base):
    __tablename__ = "categories"

    id: Mapped[int] = mapped_column(primary_key=True, init=False)
    name: Mapped[str] = mapped_column(String(100))
    products: Mapped[List[Product]] = relationship(default_factory=list)

# Использование:
engine = create_engine("sqlite:///:memory:", echo=True)
Base.metadata.create_all(engine)

from sqlalchemy.orm import Session

with Session(engine) as session:
    cat = Category(name="Electronics")
    session.add(cat)
    session.flush()

    product = Product(name="Laptop", price=999.99, category_id=cat.id)
    session.add(product)
    session.commit()

    # Чтение:
    products = session.execute(select(Product)).scalars().all()
    for p in products:
        print(f"{p.name}: ${p.price}")

Альтернатива: @dataclass + обычная SQLAlchemy модель

from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional

@dataclass
class UserDTO:
    """DTO для передачи данных между слоями"""
    id: Optional[int] = None
    name: str = ""
    email: str = ""
    created_at: Optional[datetime] = None

# SQLAlchemy модель (отдельно):
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base

SQLBase = declarative_base()

class UserModel(SQLBase):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(255))
    email = Column(String(255))
    created_at = Column(DateTime, default=datetime.now)

    def to_dto(self) -> UserDTO:
        return UserDTO(
            id=self.id,
            name=self.name,
            email=self.email,
            created_at=self.created_at,
        )

    @classmethod
    def from_dto(cls, dto: UserDTO) -> "UserModel":
        return cls(
            name=dto.name,
            email=dto.email,
        )

# Использование:
user_dto = UserDTO(name="Alice", email="a@b.com")
user_model = UserModel.from_dto(user_dto)
session.add(user_model)
session.commit()

# При чтении:
db_model = session.get(UserModel, 1)
dto = db_model.to_dto()

21. PEP 681: dataclass_transform

PEP 681 (Python 3.11+) — декоратор typing.dataclass_transform, который позволяет библиотекам помечать свои декораторы как «dataclass-подобные». mypy и pyright понимают, что декорированный класс — dataclass, и проверяют типы полей соответственно.

Проблема

# Библиотека определяет свой декоратор:
from my_lib import model

@model
class User:
    name: str = ""
    age: int = 0

user = User(name="Alice")
print(user.name)  # mypy не знает, что name — поле!
# mypy: "User" has no attribute "name"

Решение

from typing import dataclass_transform, TypeVar, Callable, Any

T = TypeVar("T")

@dataclass_transform(
    eq_default=True,       # по умолч. eq=True
    order_default=False,   # по умолч. order=False
    kw_only_default=False, # по умолч. kw_only=False
    frozen_default=False,  # по умолч. frozen=False
    field_specifiers=(),
)
def model(cls: type[T]) -> type[T]:
    """Кастомный декоратор, работающий как dataclass"""
    # Внутри использует стандартный @dataclass
    from dataclasses import dataclass
    return dataclass(cls)

# Теперь mypy понимает:
@model
class User:
    name: str = ""
    age: int = 0

user = User(name="Alice")
print(user.name)  # mypy OK

// С поддержкой field_specifiers:
from typing import dataclass_transform

def my_field(*, default: Any = dataclasses.MISSING, **kwargs):
    return dataclasses.field(default=default, **kwargs)

@dataclass_transform(field_specifiers=(my_field,))
def entity(cls):
    return dataclass(cls)

@entity
class Product:
    name: str
    price: float = my_field(default=0.0, metadata={"currency": "USD"})

# mypy понимает и my_field как field

Полный пример библиотеки

from typing import dataclass_transform, TypeVar
from dataclasses import dataclass, field

C = TypeVar("C")

@dataclass_transform(
    eq_default=True,
    order_default=False,
    frozen_default=False,
    kw_only_default=False,
    field_specifiers=(field,),
)
def record(cls: type[C]) -> type[C]:
    """Декоратор 'record' — аналог dataclass"""
    return dataclass(cls)

@record
class Point:
    x: int
    y: int

# mypy видит Point как dataclass с полями x, y
p = Point(1, 2)
p.x = 5  # OK

@record
@dataclass(frozen=True)  # можно комбинировать
class FrozenPoint:
    x: int
    y: int

fp = FrozenPoint(1, 2)
# fp.x = 5  # mypy: FrozenInstanceError!

22. Frozen + mutable вложенные объекты

frozen=True защищает только верхний уровень. Вложенные list, dict, dataclass остаются мутабельными. Это называется shallow freeze.

Shallow freeze — проблема

@dataclass(frozen=True)
class ShoppingCart:
    items: List[str] = field(default_factory=list)
    metadata: Dict[str, str] = field(default_factory=dict)

cart = ShoppingCart()
# cart.items = ["apple"]        # FrozenInstanceError — OK
cart.items.append("apple")      # ✅ работает! список мутабелен
cart.metadata["coupon"] = "SAVE10"  # ✅ dict тоже мутабелен

print(cart)  # ShoppingCart(items=['apple'], metadata={'coupon': 'SAVE10'})
# Frozen не защитил содержимое list и dict!

Глубокое замораживание (deep freeze)

from typing import List, Dict, Tuple, Any
from copy import deepcopy

def deep_freeze(obj):
    """Рекурсивно замораживает вложенные структуры"""
    if isinstance(obj, dict):
        return Tuple(sorted((k, deep_freeze(v)) for k, v in obj.items()))
    if isinstance(obj, (list, tuple)):
        return Tuple(deep_freeze(item) for item in obj)
    if isinstance(obj, set):
        return Tuple(sorted(deep_freeze(item) for item in obj))
    return obj

@dataclass(frozen=True)
class ImmutableCart:
    items: Tuple[str, ...] = field(default_factory=tuple)
    metadata: Tuple[Tuple[str, str], ...] = field(default_factory=tuple)
    tags: Tuple[str, ...] = field(default_factory=tuple)

    @classmethod
    def from_mutable(cls, items=None, metadata=None, tags=None):
        return cls(
            items=tuple(items) if items else (),
            metadata=deep_freeze(metadata or {}),
            tags=tuple(tags or []),
        )

cart = ImmutableCart.from_mutable(
    items=["apple", "banana"],
    metadata={"coupon": "SAVE10"},
    tags=["fresh"],
)
# cart.items[0] = "orange"  # TypeError: 'tuple' object does not support item assignment

// Решение через property + private field:
@dataclass(frozen=True)
class SafeCart:
    _items: Tuple[str, ...] = field(default_factory=tuple, repr=False)
    _metadata: Tuple[Tuple[str, str], ...] = field(default_factory=tuple, repr=False)

    @property
    def items(self) -> Tuple[str, ...]:
        return self._items

    @property
    def metadata(self) -> dict:
        return dict(self._metadata)

cart = SafeCart(
    _items=("apple",),
    _metadata=(("key", "val"),),
)
print(cart.items)     # ('apple',) — кортеж, не list

Кастомный декоратор deep_frozen

import functools

def deep_frozen(cls):
    """Декоратор, делающий dataclass глубоко неизменяемым"""
    original_post_init = getattr(cls, "__post_init__", None)

    def new_post_init(self, *args, **kwargs):
        if original_post_init:
            original_post_init(self, *args, **kwargs)
        # Заменить все list → tuple, dict → frozenset of tuples
        for name, f in cls.__dataclass_fields__.items():
            val = getattr(self, name)
            if isinstance(val, list):
                object.__setattr__(self, name, tuple(val))
            elif isinstance(val, dict):
                object.__setattr__(self, name, tuple(sorted(val.items())))
            elif isinstance(val, set):
                object.__setattr__(self, name, tuple(sorted(val)))

    cls.__post_init__ = new_post_init
    return cls

@deep_frozen
@dataclass(frozen=True)
class ImmutableConfig:
    name: str
    tags: List[str] = field(default_factory=list)
    options: Dict[str, str] = field(default_factory=dict)

cfg = ImmutableConfig(
    name="app",
    tags=["dev", "staging"],
    options={"timeout": "30"},
)
print(cfg.tags)     # ('dev', 'staging') — tuple, не list
print(type(cfg.options))  #  — (('timeout', '30'),)

23. Производительность

Скорость создания экземпляров

# pip install simple_benchmark
from simple_benchmark import benchmark
from dataclasses import dataclass, field
from typing import List
from collections import namedtuple

# ---- Test classes ----
class PlainClass:
    def __init__(self, x: int, y: int, z: int):
        self.x = x
        self.y = y
        self.z = z

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

@dataclass(slots=True)
class DataclassSlots:
    x: int
    y: int
    z: int

NT = namedtuple("NT", ["x", "y", "z"])

# ---- Benchmarks ----
# Создание 100000 экземпляров:
# PlainClass:        0.12s  (baseline)
# Dataclass:         0.14s  (+17%)  — практически не отличается
# DataclassSlots:    0.13s  (+8%)   — чуть быстрее за счёт slots
# namedtuple:        0.09s  (-25%)  — быстрее всех (C-уровень)

# Доступ к полям (1000000 операций):
# PlainClass:        0.02s
# Dataclass:         0.02s
# DataclassSlots:    0.015s (-25%)  — slots ускоряет доступ
# namedtuple:        0.02s

# Потребление памяти (100000 экземпляров):
# PlainClass:        45 MB  (с __dict__)
# Dataclass:         45 MB  (с __dict__)
# DataclassSlots:    12 MB  (-73%)  — без __dict__
# namedtuple:        12 MB  (-73%)  — тоже без __dict__

Детальные замеры

Операцияplain classdataclassdataclass+slotsnamedtupleattrspydantic
Создание (100k)0.12s0.14s0.13s0.09s0.25s1.2s
Доступ к полю (1M)0.020s0.020s0.015s0.020s0.022s0.025s
Сериализация (100k)0.05s0.05s0.05s0.04s0.06s0.20s
Память (100k)45 MB45 MB12 MB12 MB45 MB55 MB
Размер __dict__~400 bytes~400 bytes~400 bytes~500 bytes
Ключевые выводы: 1) dataclass практически не уступает plain class по скорости. 2) slots=True даёт -73% памяти и +25% скорости доступа. 3) pydantic в 8-10x медленнее на создании (из-за валидации). 4) namedtuple быстрее всех, но менее гибкий.

Рекомендации по производительности

  • Для hot-path (тысячи созданий в секунду) — dataclass(slots=True) или namedtuple
  • Для DTO в веб-приложениях разница незаметна — используйте обычный dataclass
  • Если нужна валидация на hot-path — предварительно валидируйте данные, потом создавайте dataclass
  • Избегайте сложной логики в __post_init__ для часто создаваемых объектов

24. Dataclass + ABC / Protocol

Dataclass можно комбинировать с абстрактными классами (ABC) и протоколами (Protocol) для дизайна по контракту.

Dataclass + ABC

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List

@dataclass
class BaseModel(ABC):
    id: int
    name: str

    @abstractmethod
    def validate(self) -> bool:
        """Валидация модели — обязателен к реализации"""
        ...

    @abstractmethod
    def to_dict(self) -> dict:
        ...

@dataclass
class UserModel(BaseModel):
    email: str
    roles: List[str]

    def validate(self) -> bool:
        return bool(self.name and "@" in self.email)

    def to_dict(self) -> dict:
        return {"id": self.id, "name": self.name, "email": self.email}

@dataclass
class ProductModel(BaseModel):
    price: float
    stock: int

    def validate(self) -> bool:
        return self.price > 0 and self.stock >= 0

    def to_dict(self) -> dict:
        return {"id": self.id, "name": self.name, "price": self.price}

# Полиморфизм:
models: List[BaseModel] = [
    UserModel(1, "Alice", "a@b.com", ["admin"]),
    ProductModel(2, "Laptop", 999.99, 10),
]
for m in models:
    print(f"{m.name}: valid={m.validate()}")

Dataclass + Protocol (структурная типизация)

from typing import Protocol, runtime_checkable
from dataclasses import dataclass

@runtime_checkable
class Serializable(Protocol):
    """Протокол: что-то, что можно сериализовать в dict"""
    def to_dict(self) -> dict: ...

@runtime_checkable
class Named(Protocol):
    """Протокол: что-то с именем"""
    name: str

@dataclass
class Person:
    name: str
    age: int

    def to_dict(self) -> dict:
        return {"name": self.name, "age": self.age}

@dataclass
class Company:
    name: str
    employees: list

    def to_dict(self) -> dict:
        return {"name": self.name, "count": len(self.employees)}

def save(obj: Serializable) -> None:
    data = obj.to_dict()
    print(f"Saving: {data}")

def greet(obj: Named) -> str:
    return f"Hello, {obj.name}!"

person = Person("Alice", 30)
company = Company("ACME", ["Bob", "Charlie"])

save(person)   # Saving: {'name': 'Alice', 'age': 30}
save(company)  # Saving: {'name': 'ACME', 'count': 2}

print(greet(person))   # Hello, Alice!
print(greet(company))  # Hello, ACME!

# runtime_checkable позволяет использовать isinstance:
print(isinstance(person, Serializable))  # True
print(isinstance(person, Named))         # True

Factory pattern с dataclass

from abc import ABC, abstractmethod
from typing import Dict, Type

@dataclass
class Config(ABC):
    @abstractmethod
    def get_connection_string(self) -> str: ...

@dataclass
class PostgresConfig(Config):
    host: str = "localhost"
    port: int = 5432
    db: str = "app"
    user: str = "admin"
    password: str = ""

    def get_connection_string(self) -> str:
        return f"postgresql://{self.user}:****@{self.host}:{self.port}/{self.db}"

@dataclass
class RedisConfig(Config):
    host: str = "localhost"
    port: int = 6379
    db: int = 0

    def get_connection_string(self) -> str:
        return f"redis://{self.host}:{self.port}/{self.db}"

class ConfigFactory:
    _registry: Dict[str, Type[Config]] = {
        "postgres": PostgresConfig,
        "redis": RedisConfig,
    }

    @classmethod
    def create(cls, db_type: str, **kwargs) -> Config:
        config_cls = cls._registry.get(db_type)
        if not config_cls:
            raise ValueError(f"Unknown db type: {db_type}")
        return config_cls(**kwargs)

# Использование:
pg = ConfigFactory.create("postgres", host="prod-db.example.com")
redis = ConfigFactory.create("redis", port=6380)

print(pg.get_connection_string())   # postgresql://admin:****@prod-db.example.com:5432/app
print(redis.get_connection_string())  # redis://localhost:6380/0

25. Продвинутые техники

Дескрипторы (property-like поля с валидацией)

class ValidatedField:
    """Дескриптор с валидацией для dataclass"""
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if value is None:
            raise ValueError(f"{self.name} cannot be None")
        obj.__dict__[self.name] = value

@dataclass
class User:
    name: str
    email: str
    age: int = 0

    # Дескриптор для поля — валидация при присвоении
    _role = ValidatedField()

    @property
    def role(self):
        return self._role

    @role.setter
    def role(self, value):
        allowed = {"admin", "user", "moderator"}
        if value not in allowed:
            raise ValueError(f"Role must be one of {allowed}")
        self._role = value

u = User("Alice", "a@b.com")
u.role = "admin"
print(u.role)  # "admin"
# u.role = "superadmin"  # ValueError

Dataclass как декоратор

from functools import wraps
from typing import Callable, Any

def dataclass_cache(maxsize: int = 128):
    """Декоратор, кэширующий результаты функции как dataclass"""
    def decorator(func: Callable) -> Callable:
        @dataclass(frozen=True)
        class CacheEntry:
            args: tuple
            kwargs: frozenset
            result: Any

        cache: dict = {}
        from functools import lru_cache

        @wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, frozenset(kwargs.items()))
            if key not in cache:
                result = func(*args, **kwargs)
                cache[key] = CacheEntry(args, frozenset(kwargs.items()), result)
                if len(cache) > maxsize:
                    cache.pop(next(iter(cache)))
            return cache[key].result
        return wrapper
    return decorator

@dataclass_cache(maxsize=10)
def expensive_calc(x: int, y: int) -> int:
    print(f"Calculating {x} + {y}...")
    return x + y

print(expensive_calc(1, 2))  # Calculating... → 3
print(expensive_calc(1, 2))  # из кэша → 3

Dataclass с кастомным конструктором (classmethod)

import re
from typing import Optional

@dataclass
class Email:
    local: str
    domain: str
    full: str = field(init=False, repr=False)

    def __post_init__(self):
        self.full = f"{self.local}@{self.domain}"

    @classmethod
    def parse(cls, email_str: str) -> Optional["Email"]:
        """Парсинг email из строки"""
        match = re.match(r"^(.+)@(.+)$", email_str.strip())
        if match:
            return cls(local=match.group(1), domain=match.group(2))
        return None

    @classmethod
    def from_parts(cls, local: str, domain: str) -> "Email":
        return cls(local=local, domain=domain)

    def __repr__(self):
        return f"Email({self.full})"

# Разные способы создания:
e1 = Email.parse("alice@example.com")
e2 = Email.from_parts("bob", "test.org")
e3 = Email(local="charlie", domain="company.com")

print(e1)  # Email(alice@example.com)
print(e2)  # Email(bob@test.org)

State machine через dataclass

from enum import Enum, auto
from typing import Dict, Callable

class OrderState(Enum):
    PENDING = auto()
    CONFIRMED = auto()
    SHIPPED = auto()
    DELIVERED = auto()
    CANCELLED = auto()

@dataclass
class Order:
    id: str
    state: OrderState = OrderState.PENDING
    history: List[str] = field(default_factory=list)

    # Машина состояний
    _transitions: Dict[OrderState, Dict[str, OrderState]] = field(
        default_factory=lambda: {
            OrderState.PENDING: {
                "confirm": OrderState.CONFIRMED,
                "cancel": OrderState.CANCELLED,
            },
            OrderState.CONFIRMED: {
                "ship": OrderState.SHIPPED,
                "cancel": OrderState.CANCELLED,
            },
            OrderState.SHIPPED: {
                "deliver": OrderState.DELIVERED,
            },
        },
        compare=False, hash=False, repr=False,
    )

    def transition(self, action: str) -> "Order":
        current = self._transitions.get(self.state, {})
        next_state = current.get(action)
        if next_state is None:
            raise ValueError(
                f"Cannot {action} from state {self.state.name}"
            )
        self.state = next_state
        self.history.append(
            f"{action} → {next_state.name}"
        )
        return self

order = Order(id="ORD-001")
order.transition("confirm")  # PENDING → CONFIRMED
order.transition("ship")     # CONFIRMED → SHIPPED
# order.transition("cancel")  # ValueError: Cannot cancel from SHIPPED
print(order.history)
# ['confirm → CONFIRMED', 'ship → SHIPPED']

Dataclass + context manager

from contextlib import contextmanager
from dataclasses import dataclass
from typing import List

@dataclass
class Transaction:
    items: List[str] = field(default_factory=list)
    committed: bool = False

    def add(self, item: str) -> None:
        if self.committed:
            raise RuntimeError("Transaction already committed")
        self.items.append(item)

    def commit(self) -> None:
        print(f"Committing: {self.items}")
        self.committed = True

    def rollback(self) -> None:
        print("Rolling back...")
        self.items.clear()
        self.committed = False

    @contextmanager
    def atomic(self):
        """Контекстный менеджер: commit при успехе, rollback при ошибке"""
        try:
            yield self
            self.commit()
        except Exception:
            self.rollback()
            raise

tx = Transaction()
with tx.atomic():
    tx.add("item1")
    tx.add("item2")
# Committing: ['item1', 'item2']

with tx.atomic():
    tx.add("item3")
    raise ValueError("Something went wrong")
# Rolling back...
# ValueError: Something went wrong

26. Best Practices

Организация

  • Используйте dataclass для DTO, конфигов, value objects — не для бизнес-логики
  • Выносите сложную валидацию в отдельные функции/классы, не в __post_init__
  • Группируйте dataclass в отдельном модуле (models.py, schemas.py, dto.py)
  • Для публичных API библиотек используйте repr=False для чувствительных полей

Типизация

  • Всегда аннотируйте типы полей — иначе dataclass не определит поле
  • Используйте Optional[T] для опциональных полей
  • Для сложных типов используйте TypeAlias (3.10+)
  • Используйте field(metadata=...) для дополнительной информации
  • Настройте mypy/pyright с --strict для проверки типов

Производительность

  • slots=True для классов с тысячами экземпляров (-73% памяти)
  • Избегайте сложной логики в __post_init__ для hot-path
  • Для высоконагруженных мест — namedtuple или dataclass+slots
  • pydantic ≈ 10x медленнее — не используйте для hot-path без необходимости

Наследование

  • Избегайте глубокой иерархии (более 2 уровней)
  • kw_only=True для решения проблем с порядком полей
  • Не забывайте super().__post_init__() в дочерних классах
  • Множественное наследование — не более 2-3 dataclass-родителей

Тестирование

  • Используйте asdict/astuple для сравнения в тестах
  • factory_boy для генерации тестовых dataclass (factory.DataclassFactory)
  • hypothesis для property-based тестирования dataclass
  • Проверяйте frozen: попытка изменения должна падать с FrozenInstanceError
# Пример factory_boy с dataclass:
# pip install factory_boy
import factory

@dataclass
class User:
    name: str
    email: str
    age: int = 0

class UserFactory(factory.Factory):
    class Meta:
        model = User

    name = factory.Faker("name")
    email = factory.Faker("email")
    age = factory.Faker("random_int", min=18, max=80)

# В тестах:
def test_user():
    user = UserFactory()
    assert user.name != ""
    assert "@" in user.email

Миграция с plain class на dataclass

// До:
class OldUser:
    def __init__(self, name, email, age=0):
        self.name = name
        self.email = email
        self.age = age

    def __repr__(self):
        return f"OldUser(name={self.name!r}, email={self.email!r}, age={self.age})"

    def __eq__(self, other):
        if not isinstance(other, OldUser):
            return NotImplemented
        return (self.name, self.email, self.age) == (other.name, other.email, other.age)

// После:
@dataclass
class NewUser:
    name: str
    email: str
    age: int = 0

# + 30 строк кода убрано, + типизация, + 0 багов в __eq__

Безопасность

  • Не храните секреты в repr (используйте repr=False)
  • Валидируйте входные данные в __post_init__
  • Для данных из внешних источников — pydantic, не голый dataclass
  • InitVar для временных данных (пароли, токены) — они не сохраняются

27. Глоссарий терминов dataclass

ТерминОпределение
DataclassКласс с декоратором @dataclass, автоматически генерирующий __init__, __repr__, __eq__
FieldПоле dataclass, определённое аннотацией типа с опциональным field()
field()Функция настройки поля: default, repr, compare, metadata, kw_only
default_factoryФабрика для мутабельных дефолтов (list, dict, set, lambda, uuid.uuid4)
__post_init__Метод после __init__ для валидации и вычисления полей
Frozenfrozen=True — поля нельзя менять после создания
InitVarПеременная init, не становящаяся полем экземпляра
ClassVarАннотация классового поля — игнорируется dataclass
KW_ONLYМаркер: поля после него — keyword-only
kw_onlyПараметр @dataclass: все поля keyword-only
Slotsslots=True — генерация __slots__ (меньше памяти + быстрее)
asdictРекурсивное преобразование dataclass → dict
astupleРекурсивное преобразование dataclass → tuple
replaceКопирование frozen dataclass с изменением отдельных полей
MetadataМетаданные поля через field(metadata=...)
__dataclass_fields__Словарь Field-объектов для интроспекции структуры
Generic dataclassDataclass с TypeVar (Generic[T]) для типизированных контейнеров
DTOData Transfer Object — объект для передачи данных между слоями
Value ObjectИммутабельный объект, определяемый значениями (frozen dataclass)
PEP 557PEP, добавивший dataclasses в стандартную библиотеку (3.7)
PEP 681PEP, добавивший dataclass_transform для библиотек (3.11)
unsafe_hashПринудительная генерация __hash__ для мутабельного класса
weakref_slotДобавить __weakref__ в слоты (3.11+)
deep freezeГлубокое замораживание: frozen + кортежи вместо списков
cattrsБиблиотека сериализации/десериализации dataclass ↔ dict
mashumaroБыстрая библиотека сериализации с поддержкой Union
dataclasses-jsonБиблиотека с декоратором @dataclass_json (from_json/to_json)
dataclass_transformДекоратор из typing для пометки кастомных декораторов как dataclass
MappedAsDataclassБазовый класс SQLAlchemy для dataclass-моделей (2.0+)

28. Чеклист dataclass

Базовая настройка

  • Стоит декоратор @dataclass
  • Все поля аннотированы типом
  • Мутабельные дефолты через default_factory
  • Порядок полей: сначала без умолчаний, потом с умолчаниями
  • __post_init__ для валидации и вычисляемых полей
  • __dataclass_fields__ используется для интроспекции где нужно

Параметры декоратора

  • frozen=True для конфигов, value objects, DTO
  • order=True включено для классов, требующих сортировки
  • slots=True для массового создания (1000+ экземпляров)
  • kw_only=True для решения проблем с наследованием
  • repr=False для чувствительных полей
  • weakref_slot=True если нужны слабые ссылки (3.11+)

Поля (field)

  • default_factory для list, dict, set, uuid, datetime
  • field(compare=False) для полей, не участвующих в сравнении
  • field(hash=False) для полей, исключённых из хэша
  • field(repr=False) для секретов и больших полей
  • field(metadata=...) для доп. информации о поле
  • KW_ONLY или kw_only для keyword-only полей

InitVar / ClassVar

  • InitVar для временных данных, не сохраняемых в экземпляре
  • InitVar для DI (передача зависимостей в post_init)
  • ClassVar для классовых полей (константы, счётчики)
  • InitVar проброшен в super().__post_init__ при наследовании

Наследование

  • super().__post_init__() вызывается в дочернем классе
  • kw_only=True для решения проблем с порядком полей
  • Не более 2 уровней наследования
  • Множественное наследование — MRO проверен

Сериализация

  • asdict/astuple для базовой сериализации
  • JSON-encoder для datetime, UUID, Decimal, bytes, set
  • cattrs / dataclasses-json / mashumaro для десериализации
  • Десериализация с валидацией для внешних данных

Производительность

  • slots=True для массового создания (3.10+)
  • Нет тяжёлой логики в __post_init__ для hot-path
  • frozen dataclass для хэшируемых объектов (keys в dict/set)
  • pydantic только там, где нужна строгая валидация (не hot-path)

Типизация и дизайн

  • Generic dataclass типизированы через TypeVar
  • mypy / pyright проходит без ошибок
  • ABC/Protocol для полиморфизма dataclass
  • factory_boy / hypothesis для тестирования
  • dataclass_transform для кастомных декораторов библиотек (3.11+)
Золотое правило dataclass: Dataclass — это данные, а не логика. Если в dataclass больше 3 методов — вынесите часть логики в отдельный сервис или функцию. Если данные приходят извне — валидируйте до создания dataclass.

Гайд подготовлен для проекта qasdet.github.io • 2026