Flask — веб-фреймворк на Python

Flask — лёгкий и гибкий микрофреймворк для создания веб-приложений и REST API на Python. Идеален для прототипирования и небольших проектов.

Уровень: средний 18 разделов ⏱ 2–3 часа

1. Что такое Flask

Flask — это микрофреймворк для веб-разработки на Python, основанный на стандарте WSGI. Название «микро» не означает, что он ограничен: Flask намеренно не навязывает структуру проекта, оставляя выбор за разработчиком.

Когда выбирать Flask

СценарийFlaskDjango
Прототип / MVPотлично подходитперегрузка
REST APIочень удобнонужны пакеты
Большой проектнужна самосборка«батарейки» из коробки
Обучениеминимальный порог входамного концепций сразу
Flask не имеет встроенного ORM, админки или системы миграций — всё подключается отдельными пакетами по необходимости.

2. Установка и первое приложение

Создай виртуальное окружение и установи Flask:

python -m venv venv
venv\Scripts\activate
pip install flask

Создай файл app.py с минимальным приложением:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Привет, мир!"

if __name__ == "__main__":
    app.run(debug=True)

Запусти сервер:

python app.py

Открой браузер по адресу http://127.0.0.1:5000 — увидишь приветствие.

Параметр debug=True включает режим отладки: автоматическую перезагрузку при изменении кода и подробные страницы ошибок. Не используй его в продакшене.

Как это работает

  1. Flask(__name__) создаёт экземпляр приложения.
  2. @app.route("/") — декоратор, связывающий URL-адрес с функцией-представлением.
  3. app.run() запускает встроенный development-сервер Werkzeug.

3. Маршруты и переменные пути

Маршрут определяется декоратором @app.route(). Внутри URL можно задать переменные:

@app.route("/user/<int:user_id>")
def profile(user_id):
    return f"Профиль пользователя #{user_id}"

@app.route("/post/<slug:slug>")
def article(slug):
    return f"Статья: {slug}"

@app.route("/files/<path:filepath>")
def file_page(filepath):
    return f"Файл: {filepath}"
КонвертерТипПример URL
stringстрока (по умолчанию)/user/alice
intцелое число/user/42
floatчисло с плавающей точкой/price/9.99
pathстрока с разделителями //files/a/b/c.txt
uuidUUID/item/550e8400-e29b

Генерация ссылок через url_for

Вместо хардкода URL используй url_for — она генерирует адрес по имени функции:

from flask import url_for

@app.route("/dashboard")
def dashboard():
    link = url_for("profile", user_id=7)
    return f"Ссылка: {link}"

url_for("profile", user_id=5)    # "/user/5"
url_for("article", slug="flask")  # "/post/flask"
Всегда используй url_for вместо строковых URL. Если ты изменишь маршрут, ссылки обновятся автоматически.

4. HTTP-методы

По умолчанию Flask обрабатывает только GET-запросы. Чтобы принимать POST-запросы (или другие), укажи их явно:

from flask import Flask, request

app = Flask(__name__)

@app.route("/feedback", methods=["GET", "POST"])
def feedback():
    if request.method == "POST":
        name = request.form["name"]
        message = request.form["message"]
        return f"Спасибо, {name}! Сообщение: {message}"
    return "<h1>Форма обратной связи</h1>"

Объект request

АтрибутОписание
request.methodHTTP-метод запроса
request.formданные из HTML-формы (POST)
request.argsпараметры query-string (?key=value)
request.jsonтело запроса как JSON
request.filesзагруженные файлы
request.headersHTTP-заголовки
request.cookiescookies клиента

5. Шаблоны Jinja2

Flask использует Jinja2 для рендеринга HTML. Шаблоны хранятся в папке templates/ рядом с app.py.

Базовый шаблон

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>{% block title %}Flask App{% endblock %}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  <nav>
    <a href="{{ url_for('index') }}">Главная</a>
  </nav>

  {% block content %}{% endblock %}

  <footer>&copy; 2026</footer>
</body>
</html>

Дочерний шаблон

{% extends "base.html" %}

{% block title %}Профиль{% endblock %}

{% block content %}
  <h1>Привет, {{ user_name }}!</h1>

  {% if posts %}
    <ul>
    {% for post in posts %}
      <li>{{ post.title }} ({{ post.date }})</li>
    {% endfor %}
    </ul>
  {% else %}
    <p>Пока нет записей.</p>
  {% endif %}
{% endblock %}

Синтаксис Jinja2

@app.route("/dashboard")
def dashboard():
    from flask import render_template
    user = {"name": "Анна", "role": "админ"}
    items = ["Задача 1", "Задача 2", "Задача 3"]
    return render_template("dashboard.html", user=user, items=items)

6. Формы и обработка данных

Создадим форму входа. HTML-форма отправляет данные методом POST:

<form method="POST" action="{{ url_for('login') }}">
  <label>Логин</label>
  <input type="text" name="username" required>

  <label>Пароль</label>
  <input type="password" name="password" required>

  <button type="submit">Войти</button>
</form>

Обработка формы на сервере

from flask import Flask, request, redirect, url_for, flash, session

app = Flask(__name__)
app.secret_key = "секретный-ключ-для-сессий"

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username", "").strip()
        password = request.form.get("password", "")

        if not username or not password:
            flash("Заполните все поля", "error")
            return redirect(url_for("login"))

        session["user"] = username
        flash(f"Добро пожаловать, {username}!", "success")
        return redirect(url_for("dashboard"))

    return render_template("login.html")

@app.route("/logout")
def logout():
    session.pop("user", None)
    flash("Вы вышли из аккаунта", "info")
    return redirect(url_for("index"))

Вывод flash-сообщений в шаблоне

{% with messages = get_flashed_messages(with_categories=true) %}
  {% if messages %}
    {% for category, msg in messages %}
      <div class="alert alert-{{ category }}">{{ msg }}</div>
    {% endfor %}
  {% endif %}
{% endwith %}
Всегда задавай app.secret_key перед использованием session и flash. Без этого Flask выбросит ошибку RuntimeError.

7. Работа с базой данных

Flask не включает ORM, но можно использовать SQLAlchemy через расширение Flask-SQLAlchemy, либо работать с sqlite3 напрямую.

Подключение через Flask-SQLAlchemy

pip install flask-sqlalchemy
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///notes.db"
db = SQLAlchemy(app)

class Note(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    text = db.Column(db.Text, default="")

with app.app_context():
    db.create_all()

CRUD-операции

@app.route("/notes")
def notes_list():
    all_notes = Note.query.all()
    return render_template("notes.html", notes=all_notes)

@app.route("/notes/create", methods=["POST"])
def create_note():
    title = request.form["title"]
    text = request.form.get("text", "")
    note = Note(title=title, text=text)
    db.session.add(note)
    db.session.commit()
    flash("Заметка создана")
    return redirect(url_for("notes_list"))

@app.route("/notes/<int:note_id>/edit", methods=["POST"])
def edit_note(note_id):
    note = Note.query.get_or_404(note_id)
    note.title = request.form["title"]
    note.text = request.form.get("text", "")
    db.session.commit()
    flash("Заметка обновлена")
    return redirect(url_for("notes_list"))

@app.route("/notes/<int:note_id>/delete", methods=["POST"])
def delete_note(note_id):
    note = Note.query.get_or_404(note_id)
    db.session.delete(note)
    db.session.commit()
    flash("Заметка удалена")
    return redirect(url_for("notes_list"))
Для работы с sqlite3 напрямую используй import sqlite3 и контекстный менеджер with sqlite3.connect("db.sqlite") as conn:. Это хороший вариант для простых проектов без ORM.

8. JSON API на Flask

Flask отлично подходит для создания REST API. Функция jsonify превращает словарь Python в JSON-ответ.

from flask import Flask, request, jsonify

app = Flask(__name__)

notes = []
next_id = 1

@app.route("/api/notes", methods=["GET"])
def api_get_notes():
    return jsonify(notes), 200

@app.route("/api/notes", methods=["POST"])
def api_create_note():
    global next_id
    data = request.get_json()

    if not data or "title" not in data:
        return jsonify({"error": "Поле title обязательно"}), 400

    note = {
        "id": next_id,
        "title": data["title"],
        "text": data.get("text", ""),
    }
    next_id += 1
    notes.append(note)
    return jsonify(note), 201

@app.route("/api/notes/<int:note_id>", methods=["PUT"])
def api_update_note(note_id):
    data = request.get_json()
    for note in notes:
        if note["id"] == note_id:
            note["title"] = data.get("title", note["title"])
            note["text"] = data.get("text", note["text"])
            return jsonify(note), 200
    return jsonify({"error": "Заметка не найдена"}), 404

@app.route("/api/notes/<int:note_id>", methods=["DELETE"])
def api_delete_note(note_id):
    global notes
    before = len(notes)
    notes = [n for n in notes if n["id"] != note_id]
    if len(notes) < before:
        return jsonify({"message": "Удалено"}), 200
    return jsonify({"error": "Заметка не найдена"}), 404

Популярные HTTP-коды

КодЗначениеКогда использовать
200OKуспешный GET/PUT
201Createdуспешный POST (создан ресурс)
400Bad Requestневалидные данные от клиента
404Not Foundресурс не найден
500Server Errorвнутренняя ошибка сервера
Для серьёзных API рассмотри расширение flask-restx или переходи на FastAPI, который имеет встроенную генерацию документации OpenAPI.

9. Обработка ошибок и статичные файлы

Flask позволяет переопределять стандартные страницы ошибок и раздавать статические файлы (CSS, JS, изображения).

Кастомные страницы ошибок

from flask import Flask, render_template
from werkzeug.exceptions import abort

app = Flask(__name__)

@app.errorhandler(404)
def page_not_found(e):
    return render_template("404.html"), 404

@app.errorhandler(500)
def server_error(e):
    return render_template("500.html"), 500

@app.route("/secret")
def secret():
    abort(403)
{% extends "base.html" %}

{% block title %}Страница не найдена{% endblock %}

{% block content %}
  <h1>404</h1>
  <p>Такой страницы не существует.</p>
  <a href="{{ url_for('index') }}">На главную</a>
{% endblock %}

Статические файлы

По умолчанию Flask раздаёт файлы из папки static/:

my_project/
├── app.py
├── static/
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── main.js
│   └── images/
│       └── logo.png
└── templates/
    └── base.html

Подключи статику в шаблоне:

<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<img src="{{ url_for('static', filename='images/logo.png') }}">
Папки static/ и templates/ должны находиться рядом с app.py или корнем пакета приложения.

10. Структура проекта и деплой

Для небольших приложений структура файла app.py достаточно. Для более крупных проектов используется Blueprint — способ организации маршрутов по модулям.

Blueprint

from flask import Blueprint

auth_bp = Blueprint("auth", __name__, url_prefix="/auth")

@auth_bp.route("/login", methods=["GET", "POST"])
def login():
    ...

@auth_bp.route("/register", methods=["GET", "POST"])
def register():
    ...
from auth import auth_bp

app = Flask(__name__)
app.register_blueprint(auth_bp)

Финальная структура проекта

my_flask_app/
├── venv/
├── app.py
├── auth.py
├── models.py
├── requirements.txt
├── static/
│   ├── css/
│   └── js/
└── templates/
    ├── base.html
    ├── index.html
    └── 404.html

Зависимости

pip freeze > requirements.txt

Пример содержимого requirements.txt:

Flask==3.1.1
flask-sqlalchemy==3.1.1
gunicorn==23.0.0

Запуск через Gunicorn

Gunicorn — продакшен-сервер для Python WSGI-приложений. Он заменяет встроенный сервер Flask и управляется системными демонами.

pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 app:app
ПараметрОписание
-w 44 рабочих процесса
-b 0.0.0.0:8000привязка к адресу и порту
app:appимя модуля : имя переменной приложения
Для развёртывания на VPS используй systemd-сервис или Docker. Для быстрого деплоя рассмотри Railway, Render или Fly.io — они поддерживают Python из коробки.

11. Тестирование Flask-приложения

Встроенный test_client эмулирует запросы без запуска сервера — это основа тестов Flask. Вместе с pytest получается быстрый цикл проверки:

pip install pytest
# test_app.py
from app import app

def test_index():
    client = app.test_client()
    r = client.get("/")
    assert r.status_code == 200
    assert "Привет" in r.get_data(as_text=True)

def test_form():
    client = app.test_client()
    r = client.post("/login", data={"username": "ann", "password": "123"})
    assert r.status_code == 302   # редирект после входа

app.config["TESTING"] = True включает тестовый режим: ошибки всплывают сразу, а не превращаются в 500-страницы.

Для тестов с базой подними временную SQLite в tmp-папке pytest — тесты станут независимыми и быстрыми.
Запускай тесты в CI после линта: тот же pytest в GitHub Actions или GitLab CI.

12. Flask-WTF: формы с CSRF-защитой

Ручная обработка форм из раздела 6 подходит для основ, но в продакшене нужна защита от подделки запросов (CSRF). Расширение Flask-WTF даёт и валидацию, и токен:

pip install flask-wtf
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Length

class LoginForm(FlaskForm):
    username = StringField("Логин", validators=[DataRequired(), Length(min=3)])
    password = PasswordField("Пароль", validators=[DataRequired()])
    submit = SubmitField("Войти")
@app.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        return redirect(url_for("dashboard"))
    return render_template("login.html", form=form)

Токен в шаблоне подставляется автоматически через {{ form.hidden_tag() }}.

Не отключай CSRF-проверку там, где форма принимает пароль или данные оплаты: без токена кто угодно может выполнить запрос от имени авторизованного пользователя.
WTForms — это и валидация типов: поля форм превращают строки в int, date и т.д., избавляя от ручных приводов в обработчике.

13. Безопасность: сессии, пароли, заголовки

Flask хранит сессии в подписанной cookie. app.secret_key — ключ подписи: если он утечёт, злоумышленник сможет подделывать сессии. Секрет задают из переменной окружения:

import os
app.secret_key = os.environ.get("SECRET_KEY", "dev-tmp-key")
УгрозаЗащита
подделка сессийслучайный секрет + cookie только по HTTPS
утечка паролейwerkzeug: generate_password_hash / check_password_hash
XSSJinja2 экранирует {{ }} по умолчанию
CSRFFlask-WTF из раздела 12
from werkzeug.security import check_password_hash, generate_password_hash

password_hash = generate_password_hash("пароль")
is_ok = check_password_hash(password_hash, "пароль")
SESSION_COOKIE_SECURE = True — отправлять cookie только по HTTPS. В локальной разработке с HTTP она не сохранится, поэтому включай значение в продакшене или используй локальный TLS.

14. Экосистема Flask и чек-лист проекта

Flask — микрофреймворк: готовность проекта зависит от подобранных расширений. Полезная экосистема:

ЗадачаРасширение
формы и CSRFFlask-WTF
база данныхFlask-SQLAlchemy
миграцииFlask-Migrate
авторизацияFlask-Login
документация APIFlask-RESTx + Marshmallow

Перед деплоем пройди по чек-листу:

Не раздувай Flask до размеров Django: если проект растёт — честно оцени переход на Django или асинхронный FastAPI.
Flask-Login или Flask-Security закроют аутентификацию из коробки. Свою авторизацию пиши только когда уверен, что не изобретаешь велосипед.

15. Миграции базы данных: Flask-Migrate

create_all() не меняет существующую схему. Flask-Migrate добавляет в Flask командный интерфейс Alembic: сравнение моделей и базы, генерация версий и откат:

pip install flask-migrate

from flask_migrate import Migrate

migrate = Migrate(app, db)
flask db init
flask db migrate -m "add users table"
flask db upgrade
flask db downgrade -1

Порядок работы: изменил модель → flask db migrate → вручную проверь сгенерированный скрипт → flask db upgrade. autogenerate умеет ошибаться на типах колонок и переименованиях — правь конфигурацию, а не полагайся на неё слепо.

Миграции в репозитории — часть командной работы: CI применяет их отдельным шагом перед тестами, прод — перед рестартом приложения.

Не удаляй уже применённые версии миграций: история нужна, чтобы воспроизвести состояние базы на любом шаге.
Перед релизом с дропом колонки сделай бэкап и прогони downgrade на тестовой базе — убедись, что откат действительно работает.

16. Кэширование и оптимизация ответов

Тяжёлые запросы и рендер шаблонов кэшируются. Расширение Flask-Caching поддерживает кэш в памяти и бэкенд Redis:

pip install flask-caching

from flask_caching import Cache

cache = Cache(app, config={
    "CACHE_TYPE": "RedisCache",
    "CACHE_REDIS_URL": "redis://localhost:6379/0"
})

@cache.cached(timeout=300)
def expensive():
    ...  # дорогие данные из базы

Ключи кэша по умолчанию строятся из имени функции и аргументов — для списков с фильтрами работает из коробки, а для сложных ключей задай make_cache_key сам.

Измеряй до и после: страница, отдающая одинаковые данные минутами, — кандидат на кэш; живая лента — нет.

17. Фоновые задачи: Celery и очереди

Отправка писем, генерация отчётов, внешние API — долгие операции не должны блокировать ответ браузера. Классика — Celery с брокером Redis или RabbitMQ:

pip install celery

# tasks.py
from celery import Celery

celery_app = Celery("tasks", broker="redis://localhost:6379/0")

@celery_app.task
def send_welcome(user_id):
    ...  # отправка письма

# в представлении
send_welcome.delay(user_id)

Воркер запускается отдельным процессом: celery -A tasks worker. delay() кладёт задачу в очередь и возвращает управление сразу — пользователь видит ответ мгновенно.

Для простых случаев хватит потока threading, но при перезапуске приложения такие задачи теряются. Очередь в брокере переживает рестарт.

Пиши задачи идемпотентными: падение воркера может повторить обработку. Отправка письма или списание денег обязаны пережить повторный вызов.
Держи воркер отдельным сервисом (контейнер, systemd) — так он масштабируется независимо от веб-приложения.

18. Типовые ошибки и профилирование

На реальных Flask-проектах чаще всего встречаются:

Профилирование: profiler Werkzeug (app.run(profiler=True)) или flask-profiler покажут тормозящие маршруты. Включи SQL-логирование — и N+1 станет очевиден по выводу запросов.

Логируй каждый маршрут с кодом ответа и временем: в связке с ELK или Prometheus это даёт картину медленных мест без гаданий.

Что дальше

Теперь ты знаешь основы Flask: маршруты, шаблоны, формы, базы данных и REST API. Рекомендуемый следующий шаг — изучение FastAPI для асинхронной разработки или Docker для контейнеризации и деплоя приложений.

← Вернуться к курсам