Flask — веб-фреймворк на Python
Flask — лёгкий и гибкий микрофреймворк для создания веб-приложений и REST API на Python. Идеален для прототипирования и небольших проектов.
1. Что такое Flask
Flask — это микрофреймворк для веб-разработки на Python, основанный на стандарте WSGI. Название «микро» не означает, что он ограничен: Flask намеренно не навязывает структуру проекта, оставляя выбор за разработчиком.
- WSGI — стандарт взаимодействия Python-приложений с веб-серверами. Flask реализует WSGI через библиотеку Werkzeug.
- Werkzeug — набор утилит для работы с HTTP: маршрутизация, запросы, ответы, отладка.
- Jinja2 — шаблонизатор, который генерирует HTML-страницы на основе шаблонов и данных.
Когда выбирать Flask
| Сценарий | Flask | Django |
|---|---|---|
| Прототип / MVP | отлично подходит | перегрузка |
| REST API | очень удобно | нужны пакеты |
| Большой проект | нужна самосборка | «батарейки» из коробки |
| Обучение | минимальный порог входа | много концепций сразу |
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 включает режим отладки: автоматическую перезагрузку при изменении кода и подробные страницы ошибок. Не используй его в продакшене.Как это работает
Flask(__name__)создаёт экземпляр приложения.@app.route("/")— декоратор, связывающий URL-адрес с функцией-представлением.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 |
| uuid | UUID | /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.method | HTTP-метод запроса |
| request.form | данные из HTML-формы (POST) |
| request.args | параметры query-string (?key=value) |
| request.json | тело запроса как JSON |
| request.files | загруженные файлы |
| request.headers | HTTP-заголовки |
| request.cookies | cookies клиента |
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>© 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
{{ variable }}— вывод переменной.{% if condition %}...{% endif %}— условный оператор.{% for item in list %}...{% endfor %}— цикл.{% block name %}...{% endblock %}— блок для наследования.{% extends "base.html" %}— наследование шаблона.
@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-sqlalchemyfrom 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"))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-коды
| Код | Значение | Когда использовать |
|---|---|---|
| 200 | OK | успешный GET/PUT |
| 201 | Created | успешный POST (создан ресурс) |
| 400 | Bad Request | невалидные данные от клиента |
| 404 | Not Found | ресурс не найден |
| 500 | Server Error | внутренняя ошибка сервера |
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 4 | 4 рабочих процесса |
| -b 0.0.0.0:8000 | привязка к адресу и порту |
| app:app | имя модуля : имя переменной приложения |
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-страницы.
12. Flask-WTF: формы с CSRF-защитой
Ручная обработка форм из раздела 6 подходит для основ, но в продакшене нужна защита от подделки запросов (CSRF). Расширение Flask-WTF даёт и валидацию, и токен:
pip install flask-wtffrom 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() }}.
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 |
| XSS | Jinja2 экранирует {{ }} по умолчанию |
| CSRF | Flask-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 — микрофреймворк: готовность проекта зависит от подобранных расширений. Полезная экосистема:
| Задача | Расширение |
|---|---|
| формы и CSRF | Flask-WTF |
| база данных | Flask-SQLAlchemy |
| миграции | Flask-Migrate |
| авторизация | Flask-Login |
| документация API | Flask-RESTx + Marshmallow |
Перед деплоем пройди по чек-листу:
- debug выключен, секреты в переменных окружения;
- зафиксирован
requirements.txt; - база управляется миграциями, а не
create_allв коде; - включены CSRF, HTTPS-параметры сессии и лимиты запросов.
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 сам.
- статику раздавай через nginx с Cache-Control — Flask не должен грузить отдачу ассетов;
- ETag экономит повторные передачи полного ответа;
- долгие вычисления выполняй фоном, а результат клади в кэш.
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, но при перезапуске приложения такие задачи теряются. Очередь в брокере переживает рестарт.
18. Типовые ошибки и профилирование
На реальных Flask-проектах чаще всего встречаются:
debug=Trueв продакшене — страницы ошибок раскрывают стек и переменные;- тяжёлый код прямо в маршруте — ответ ждёт секунды;
- N+1 запросов к базе в цикле по списку — решается JOIN-запросом;
- глобальные изменяемые переменные как «БД в памяти» теряются при рестарте;
- секрет по умолчанию вместо переменной окружения;
- нет таймаутов внешних вызовов — сервер «висит» на зависшем API.
Профилирование: profiler Werkzeug (app.run(profiler=True)) или flask-profiler покажут тормозящие маршруты. Включи SQL-логирование — и N+1 станет очевиден по выводу запросов.
Что дальше
Теперь ты знаешь основы Flask: маршруты, шаблоны, формы, базы данных и REST API. Рекомендуемый следующий шаг — изучение FastAPI для асинхронной разработки или Docker для контейнеризации и деплоя приложений.