


Dataclass в Python помогает описать объект с данными, не писать вручную конструктор, строковое представление и сравнение полей. Но аннотации типов сами по себе не проверяют значения во время выполнения. Для собственного правила, например запрета пустого названия, нужна явная проверка.
В разборе Mentoring.digital соберём модель задачи. Пример рассчитан на Python 3.10 и новее и использует стандартную библиотеку.
У задачи будут название, число попыток и метки. Поле без значения по умолчанию ставим перед полями с такими значениями. Для списка используем фабрику, которая создаёт новый объект при каждом вызове конструктора.
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
attempts: int = 0
labels: list[str] = field(default_factory=list)
def __post_init__(self):
if not isinstance(self.title, str):
raise TypeError("title должен быть строкой")
self.title = self.title.strip()
if not self.title:
raise ValueError("Нужно название задачи")
if type(self.attempts) is not int:
raise TypeError("attempts должен быть целым числом")
if self.attempts < 0:
raise ValueError("attempts не может быть отрицательным")
if not isinstance(self.labels, list):
raise TypeError("labels должен быть списком")
if any(not isinstance(x, str) for x in self.labels):
raise TypeError("Каждая метка должна быть строкой")
self.labels = self.labels.copy()
task = Task(" Проверить отчёт ")
print(task.title)
Вызов автоматически созданного __init__ запускает __post_init__. Здесь нормализуется название и проверяются данные. Проверка attempts через type исключает также True и False: в Python bool является подклассом int, но для счётчика такое значение в нашем контракте не подходит.
Запись field(default_factory=list) передаёт саму функцию list, а не результат list(). У разных задач появляются разные списки. Дополнительная copy в примере отделяет модель от списка, который вызывающий код передал явно. Это поверхностное копирование; для списка строк его достаточно.
first = Task("Отчёт")
second = Task("Отчёт")
assert first == second
first.labels.append("срочно")
assert second.labels == []
assert first != second
source = ["работа"]
third = Task("Проверка", labels=source)
source.append("новая")
assert third.labels == ["работа"]
Не задавайте labels = [] прямо в объявлении поля. Dataclass отклоняет такие изменяемые значения по умолчанию. И не считайте фабрику универсальной защитой от общих объектов: если в неё поместить функцию, возвращающую один внешний список, он снова окажется общим.
Сравнение по умолчанию учитывает поля и требует одинакового типа объектов. После изменения labels меняется и результат сравнения. Параметр frozen=True запрещает обычное присваивание полям, но не делает вложенные списки неизменяемыми. Для неизменяемых меток лучше отдельно продумать tuple и правила построения объекта.
Наш __post_init__ проверяет создание объекта, а не каждую последующую запись. Присваивание task.attempts = -1 обойдёт эту проверку. Если инвариант должен сохраняться всегда, ограничьте изменения методами, свойствами или другой моделью. Не используйте dataclass как замену проверке входного JSON.
Такие упражнения можно включить в курс Python с индивидуальной практикой. Если модель уже используется в проекте, обсудите границы проверки и изменяемость с наставником по Python.