Python: уроки, справочник и задачи

Flask формы: от request.form до Flask-WTF

Раздел: Flask и Tkinter · Обновлено 31.10.2026

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

flask формы
Фото: NPS Photo / Emily Mesner / Public domain, Wikimedia Commons
Содержание статьи
  1. Простая форма через request.form
  2. Установка и настройка Flask-WTF
  3. Класс формы, поля и валидаторы
    1. Основные типы полей
    2. Свой валидатор для поля
  4. Вывод формы в шаблоне
  5. Обработка отправки и перенаправление
  6. Загрузка файлов через форму
  7. Частые вопросы
    1. Почему validate_on_submit() всегда возвращает False?
    2. Как получить значение чекбокса из request.form?
    3. Как отключить CSRF для одной формы?
    4. Можно ли использовать WTForms без Flask-WTF?
    5. Как получить несколько значений с одинаковым name?

Простая форма через 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
StringFieldinput type="text"str
PasswordFieldinput type="password"str
TextAreaFieldtextareastr
IntegerFieldinput type="number"int или None
BooleanFieldinput type="checkbox"bool
SelectFieldselectstr (выбранное значение)
DateFieldinput type="date"datetime.date
FileFieldinput 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() только первое.