Flask формы: от request.form до Flask-WTF
Flask формы обрабатываются двумя способами: вручную через объект request.form или с помощью расширения Flask-WTF, которое добавляет классы форм, валидацию и защиту от CSRF. Ниже показан полный цикл: HTML-разметка, маршрут с методами GET и POST, проверка данных, вывод ошибок и перенаправление после отправки. Примеры рассчитаны на Flask 3 и Flask-WTF 1.2.

Содержание статьи
Простая форма через request.form
Для небольшой формы из пары полей расширения не нужны. Браузер отправляет данные методом POST, а Flask складывает их в request.form - словарь, где ключами служат атрибуты name у полей ввода. Маршрут должен явно разрешать оба метода: GET для показа страницы и POST для приема данных.
from flask import Flask, request, render_template_string
app = Flask(__name__)
PAGE = """
<form method="post">
<input name="city" placeholder="Город">
<button>Найти</button>
</form>
<p>{{ message }}</p>
"""
@app.route("/", methods=["GET", "POST"])
def index():
message = ""
if request.method == "POST":
city = request.form.get("city", "").strip()
message = f"Ищем погоду: {city}" if city else "Введите город"
return render_template_string(PAGE, message=message)Обращение request.form["city"] при отсутствии поля приводит к ответу 400 Bad Request, поэтому для необязательных полей надежнее get(). Данные из адресной строки при методе GET лежат в другом объекте, request.args. Как устроены маршруты и запуск приложения, описано в статье основы Flask.
Установка и настройка Flask-WTF
Когда полей больше и каждое нужно проверять, ручной код быстро разрастается. Расширение Flask-WTF связывает Flask с библиотекой WTForms: форма описывается классом, а проверка и сообщения об ошибках формируются автоматически.
pip install Flask-WTF email-validatorПакет email-validator нужен только для валидатора Email(), без него форма с таким полем выдаст исключение. Расширение подписывает CSRF-токены, поэтому в конфигурации обязательно задается секретный ключ.
import os
from flask import Flask
app = Flask(__name__)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-only-key")В рабочем проекте ключ должен быть длинной случайной строкой, например результатом secrets.token_hex(32), и храниться вне кода.
Класс формы, поля и валидаторы
Форма наследуется от FlaskForm, а каждое поле объявляется атрибутом класса. Первый аргумент поля - подпись, которая попадет в тег label, параметр validators принимает список проверок.
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length, NumberRange
class FeedbackForm(FlaskForm):
name = StringField("Имя", validators=[DataRequired(), Length(max=50)])
email = StringField("Email", validators=[DataRequired(), Email()])
age = IntegerField("Возраст", validators=[NumberRange(min=7, max=120)])
text = TextAreaField("Сообщение", validators=[Length(min=10)])
submit = SubmitField("Отправить")Основные типы полей
WTForms содержит поля для всех стандартных элементов HTML. Значение после проверки доступно в атрибуте data уже в нужном типе: IntegerField вернет int, DateField вернет объект date.
| Поле | HTML-элемент | Тип data |
|---|---|---|
StringField | input type="text" | str |
PasswordField | input type="password" | str |
TextAreaField | textarea | str |
IntegerField | input type="number" | int или None |
BooleanField | input type="checkbox" | bool |
SelectField | select | str (выбранное значение) |
DateField | input type="date" | datetime.date |
FileField | input type="file" | объект файла |
Свой валидатор для поля
Если встроенных проверок не хватает, в класс формы добавляют метод validate_имяполя. WTForms вызовет его автоматически, а исключение ValidationError превратится в сообщение об ошибке рядом с полем.
from wtforms.validators import ValidationError
class SignupForm(FlaskForm):
login = StringField("Логин", validators=[DataRequired()])
def validate_login(self, field):
if field.data.lower() in {"admin", "root"}:
raise ValidationError("Этот логин занят")Вывод формы в шаблоне
В шаблон передается объект формы, а каждое поле выводится вызовом. Строка form.hidden_tag() добавляет скрытое поле с CSRF-токеном: без него проверка отправки всегда будет неуспешной. Список field.errors содержит тексты ошибок валидации.
<form method="post" novalidate>
{{ form.hidden_tag() }}
{% for field in [form.name, form.email, form.age, form.text] %}
<p>
{{ field.label }}<br>
{{ field(class="input") }}
{% for error in field.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</p>
{% endfor %}
{{ form.submit() }}
</form>Атрибут novalidate отключает проверку в браузере, чтобы сообщения формировал сервер. Как выносить общие части страницы в базовый шаблон и подключать их через extends, рассказано в статье создание шаблона Flask.
Обработка отправки и перенаправление
Метод validate_on_submit() возвращает True, только если пришел POST-запрос и все проверки пройдены, включая CSRF. После успешной обработки принято делать перенаправление: тогда обновление страницы в браузере не отправит форму повторно. Этот прием называют Post/Redirect/Get.
from flask import render_template, redirect, url_for, flash
@app.route("/feedback", methods=["GET", "POST"])
def feedback():
form = FeedbackForm()
if form.validate_on_submit():
save_message(form.name.data, form.email.data, form.text.data)
flash("Спасибо, сообщение отправлено")
return redirect(url_for("feedback"))
return render_template("feedback.html", form=form)Функция save_message здесь условная: обычно данные записывают в базу, как показано в статье база данных Flask. Чтобы заполнить форму существующими значениями при редактировании, передайте объект в конструктор: FeedbackForm(obj=message).
Загрузка файлов через форму
Для отправки файла у тега формы нужен атрибут enctype="multipart/form-data". В Flask-WTF есть поле FileField и валидаторы FileRequired и FileAllowed для проверки расширения.
from flask_wtf.file import FileField, FileRequired, FileAllowed
from werkzeug.utils import secure_filename
class AvatarForm(FlaskForm):
photo = FileField("Фото", validators=[FileRequired(), FileAllowed(["jpg", "png"])])
# в обработчике после validate_on_submit():
# f = form.photo.data
# f.save(os.path.join("uploads", secure_filename(f.filename)))Функция secure_filename() убирает из имени опасные символы и пути вроде ../. Максимальный размер загрузки ограничивают настройкой MAX_CONTENT_LENGTH. Подробности о классах полей и параметрах есть в документации Flask-WTF.
Частые вопросы
Ответы на типичные проблемы при работе с формами во Flask.
Почему validate_on_submit() всегда возвращает False?
Чаще всего в шаблоне забыли form.hidden_tag() или form.csrf_token, либо не задан SECRET_KEY. Выведите form.errors в консоль, там будет точная причина.
Как получить значение чекбокса из request.form?
Неотмеченный чекбокс вообще не отправляется браузером. Проверяйте наличие ключа: "agree" in request.form.
Как отключить CSRF для одной формы?
Передайте meta={"csrf": False} в конструктор формы. Для форм, которые меняют данные на сервере, отключать защиту не стоит.
Можно ли использовать WTForms без Flask-WTF?
Можно, но тогда данные придется передавать вручную, например MyForm(request.form), а CSRF-защиту настраивать отдельно. Flask-WTF делает это за вас.
Как получить несколько значений с одинаковым name?
Используйте request.form.getlist("tags"): метод вернет список всех отправленных значений, а get() только первое.