🐛 Типичные ошибки: Pydantic v2 + SQLAlchemy 2.x

Симптом → Причина → Как исправить

⚡ Топ-3 ошибки практикума

  1. Забыть @classmethod в @field_validator — Pydantic v2 требует оба декоратора. Симптом: PydanticUserError: classmethod required.
  2. Класс SQLAlchemy не наследуется от Base — написано class User(): вместо class User(Base):. Таблица не создаётся, create_all не видит модель.
  3. Не вызвать session.commit() — изменения остаются только в памяти сессии, в БД не записываются.

Ошибки Pydantic v2

Ошибка 1: Забыть @classmethod в @field_validator

Симптом: PydanticUserError: 'classmethod' required for field_validator или предупреждение о неправильном использовании валидатора.

Причина: в Pydantic v2 @field_validator требует @classmethod. В v1 @validator мог работать без него.

# НЕПРАВИЛЬНО (v1-стиль, не работает в v2):
@field_validator('date')
def date_must_be_future(cls, v):
    ...

# ПРАВИЛЬНО (v2):
@field_validator('date')
@classmethod
def date_must_be_future(cls, v: datetime) -> datetime:
    ...

Ошибка 2: Использование constr/condecimal вместо Annotated

Симптом: ImportError: cannot import name 'constr' from 'pydantic' (в Pydantic v2 эти функции удалены или deprecated).

Причина: constr и condecimal — это функции Pydantic v1. В v2 они не существуют или вызывают предупреждения.

# НЕПРАВИЛЬНО (Pydantic v1):
from pydantic import constr, condecimal
amount: condecimal(gt=0)
tx_type: constr(regex="^(debit|credit)$")

# ПРАВИЛЬНО (Pydantic v2):
from typing import Annotated
from decimal import Decimal
from pydantic import Field
amount: Annotated[Decimal, Field(gt=0)]
tx_type: Annotated[str, Field(pattern=r"^(debit|credit)$")]

Дополнительно: в v2 параметр regex= переименован в pattern=.

Ошибка 3: Использовать class Config вместо ConfigDict

Симптом: предупреждение о deprecation или неожиданное поведение конфигурации модели.

Причина: class Config устарел в Pydantic v2. Используйте model_config = ConfigDict(...).

# НЕПРАВИЛЬНО (Pydantic v1):
class UserProfile(BaseModel):
    class Config:
        schema_extra = {"example": {...}}
        anystr_strip_whitespace = True

# ПРАВИЛЬНО (Pydantic v2):
from pydantic import ConfigDict
class UserProfile(BaseModel):
    model_config = ConfigDict(
        json_schema_extra={"example": {...}},
        str_strip_whitespace=True
    )

Ошибки SQLAlchemy 2.x

Ошибка 4: Класс не наследуется от Base

Симптом: Base.metadata.create_all(engine) не создаёт таблицу. При попытке добавить объект: AttributeError: 'Person' object has no attribute '_sa_instance_state'.

Причина: SQLAlchemy узнаёт модели только через наследование от базового класса. Класс без наследования — это просто обычный Python-класс.

# НЕПРАВИЛЬНО:
class Person():     # пустые скобки — не ORM-модель
    __tablename__ = 'persons'
    ...

# ПРАВИЛЬНО:
class Person(Base):  # наследуемся от Base
    __tablename__ = 'persons'
    ...

Ошибка 5: Не вызвать create_all перед работой с сессией

Симптом: OperationalError: no such table: users при попытке добавить данные.

Причина: объявление модели в Python не создаёт таблицу в БД. Нужно явно вызвать create_all.

# НЕПРАВИЛЬНО: пропустить create_all
with Session(engine) as session:
    session.add(User(name="Alice", age=30))  # OperationalError!

# ПРАВИЛЬНО: сначала create_all
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add(User(name="Alice", age=30))
    session.commit()

Ошибка 6: Не вызвать session.commit()

Симптом: данные исчезают после закрытия сессии. Запросы возвращают пустые результаты.

Причина: session.add() только помечает объект для добавления в текущей транзакции. Без commit() изменения не сохраняются в БД.

# НЕПРАВИЛЬНО: забыть commit()
with Session(engine) as session:
    session.add(User(name="Bob", age=25))
    # commit() не вызван — данные потеряны при закрытии сессии

# ПРАВИЛЬНО:
with Session(engine) as session:
    session.add(User(name="Bob", age=25))
    session.commit()  # зафиксировать транзакцию

Ошибка 7: String без длины

Симптом: CompileError при создании таблиц в MySQL/MariaDB. В SQLite работает, в других СУБД — нет.

Причина: MySQL требует явную длину для типа VARCHAR (String). String без аргумента в SQLAlchemy создаёт тип без длины, что не поддерживается MySQL.

# НЕПРАВИЛЬНО (проблема в MySQL):
name: Mapped[str] = mapped_column(String)        # нет длины
name = Column(String, nullable=False)             # то же

# ПРАВИЛЬНО:
name: Mapped[str] = mapped_column(String(100))    # явная длина
name: Mapped[str] = mapped_column(String(50))     # или другая

Ошибка 8: Не указать back_populates на обеих сторонах связи

Симптом: ArgumentError: Relationship 'User.posts' will copy column posts.user_id to column users.id или неожиданное поведение при обходе связи.

Причина: если back_populates="posts" указан в Post.user, то в User обязательно нужно posts = relationship(..., back_populates="user").

# НЕПРАВИЛЬНО: back_populates только с одной стороны
class User(Base):
    posts: Mapped[List["Post"]] = relationship("Post")  # нет back_populates

class Post(Base):
    user: Mapped["User"] = relationship("User", back_populates="posts")  # ошибка!

# ПРАВИЛЬНО: back_populates на обеих сторонах
class User(Base):
    posts: Mapped[List["Post"]] = relationship("Post", back_populates="user")

class Post(Base):
    user: Mapped["User"] = relationship("User", back_populates="posts")