Формы Django: валидация и cleaned_data

Формы — главный канал данных от пользователя к серверу. Django Forms берут на себя самое опасное: отрисовку, валидацию и очистку ввода.
Суть: класс Form описывает поля, Django сам рисует HTML, проверяет данные методом is_valid() и складывает очищенные значения в cleaned_data. Это защита от мусора и атак.

Зачем не писать формы руками

Можно собирать данные из request.POST вручную, но это путь к багам и уязвимостям: забыли проверить тип, не экранировали ввод, пропустили обязательное поле. Django Forms решают всё это декларативно. Вы описываете поля как класс, а фреймворк рисует HTML, валидирует ввод и возвращает чистые данные.

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)
    agree = forms.BooleanField(required=True)

Поток валидации

Форма работает в две стороны. На GET вы создаёте пустую форму и рисуете её. На POST — создаёте форму с данными и валидируете:

def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            data = form.cleaned_data   # чистые, проверенные данные
            send_email(data["email"], data["message"])
            return redirect("thanks")
    else:
        form = ContactForm()
    return render(request, "contact.html", {"form": form})

Полный поток данных формы:

POST-данные (request.POST)
      │
      ▼
ContactForm(request.POST)
      │
      ▼
form.is_valid() ──┬── False ─▶ form.errors → перерисовать форму
                  │            с сообщениями об ошибках
                  └── True  ─▶ form.cleaned_data → сохранить/обработать
                                    │
                                    ▼
                                redirect

cleaned_data и errors

После is_valid() очищенные и приведённые к типам данные лежат в form.cleaned_data (словарь). Если валидация не прошла, ошибки — в form.errors, и форму перерисовывают с подсветкой проблем. В шаблоне форму выводят так:

<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Отправить</button>
</form>

Тег {% csrf_token %} обязателен для всех POST-форм — без него Django отклонит запрос (защита от CSRF, об этом следующий урок).

Кастомная валидация

Для проверки одного поля пишут метод clean_<имя>, для проверки нескольких полей вместе — общий clean. При нарушении бросают ValidationError:

def clean_name(self):
    name = self.cleaned_data["name"]
    if "admin" in name.lower():
        raise forms.ValidationError("Имя admin запрещено")
    return name

Как это работает под капотом

Валидация — это последовательный прогон значений через набор проверок с накоплением ошибок. Это языко-независимая логика, которую видно на чистом Python:

# Попробуй сам ▶ — движок валидации формы
def validate(data):
    errors, cleaned = {}, {}
    # обязательные поля
    for field in ("name", "email", "message"):
        if not data.get(field):
            errors.setdefault(field, []).append("Обязательное поле")
    # тип email
    email = data.get("email", "")
    if email and "@" not in email:
        errors.setdefault("email", []).append("Некорректный email")
    # своя проверка clean_name
    if "admin" in data.get("name", "").lower():
        errors.setdefault("name", []).append("Имя admin запрещено")
    # собираем cleaned_data только если ошибок нет
    if not errors:
        cleaned = {k: v.strip() for k, v in data.items()}
    return (len(errors) == 0), cleaned, errors

for sample in [
    {"name": "Анна", "email": "[email protected]", "message": "Привет"},
    {"name": "admin", "email": "плохой", "message": ""},
]:
    ok, cleaned, errors = validate(sample)
    print("is_valid:", ok)
    print("  cleaned_data:", cleaned)
    print("  errors:", errors)

Django делает то же: прогоняет встроенные проверки полей, затем ваши clean_*, накапливает ошибки в form.errors и заполняет cleaned_data только при успехе.

Частые ошибки

  • Забыть {% csrf_token %}. POST-форма будет отклонена с ошибкой 403.
  • Читать request.POST напрямую вместо cleaned_data. Сырые данные не проверены и не приведены к типам.
  • Не вызвать is_valid() перед доступом к cleaned_data. Его там ещё нет.
  • В clean-методе не вернуть значение. clean_<поле> обязан вернуть очищенное значение.

Best practices

  • Всегда работайте с cleaned_data, а не с сырым request.POST.
  • Логику валидации держите в форме (clean_*/clean), а не во view.
  • Не забывайте {% csrf_token %} в каждой POST-форме.
  • Для полей, связанных с моделью, используйте ModelForm (следующий урок).

Итоги

Django Forms описывают поля декларативно, сами рисуют HTML и валидируют ввод. is_valid() проверяет данные, cleaned_data отдаёт чистые значения, errors — проблемы. Кастомные проверки — в методах clean. Это защищает приложение от мусора и атак. Дальше свяжем формы с моделями через ModelForm.

Проверьте себя
1. Откуда брать проверенные данные после успешной валидации формы?
AИз request.POST
BИз form.cleaned_data
CИз form.errors
DИз request.GET
2. Что обязательно добавить в POST-форму в шаблоне?
A{% load static %}
B{% csrf_token %}
C{% block content %}
D{% extends %}