Flask 3
🎯 Objectif : utiliser Flask 3 pour des projets ciblés (microservices, scripts web, prototypes) en assemblant les briques dont tu as besoin.
À l'issue de cet axe, tu sauras :
- Construire une app Flask avec routes, request, response
- Organiser le code avec Blueprints et l'application factory pattern
- Persister avec SQLAlchemy 2 et Flask-SQLAlchemy 3
- Sérialiser avec Marshmallow
- Authentifier avec Flask-JWT-Extended
- Tester avec le client de test natif Flask
Pourquoi Flask ?
Section intitulée « Pourquoi Flask ? »| Pour | Contre |
|---|---|
| Minimaliste : tu choisis tout (ORM, validation, auth) | Pas de magie — tout à assembler |
| Excellent pour comprendre ce qu’un framework fait vraiment | Pas de doc API auto comme FastAPI |
| Stable depuis 15+ ans | Async limité (utiliser Quart pour vraiment async) |
| Idéal pour microservices ciblés ou prototypes | À grande échelle, on assemble beaucoup d’extensions |
Verdict 2026 : Flask reste pertinent pour des projets bien définis (un endpoint webhook, un CLI HTTP, un mini-service). Pour un nouveau projet ambitieux, FastAPI est plus adapté.
Hello world Flask 3
Section intitulée « Hello world Flask 3 »uv init taskly-flaskcd taskly-flaskuv add flask flask-sqlalchemy flask-jwt-extended marshmallow
# app.pyfrom flask import Flask, jsonify
app = Flask(__name__)
@app.get("/health")def health(): return jsonify({"status": "ok"})
if __name__ == "__main__": app.run(debug=True)uv run python app.pyApplication Factory pattern
Section intitulée « Application Factory pattern »Pour les projets > 100 lignes, structure en factory plutôt qu’un seul fichier :
from flask import Flaskfrom .extensions import db, jwtfrom .config import Config
def create_app(config_class: type[Config] = Config) -> Flask: app = Flask(__name__) app.config.from_object(config_class)
db.init_app(app) jwt.init_app(app)
# Enregistrer les blueprints from .modules.auth import bp as auth_bp from .modules.tasks import bp as tasks_bp app.register_blueprint(auth_bp, url_prefix='/auth') app.register_blueprint(tasks_bp, url_prefix='/tasks')
return appfrom flask_sqlalchemy import SQLAlchemyfrom flask_jwt_extended import JWTManager
db = SQLAlchemy()jwt = JWTManager()import os
class Config: SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///./data.db") JWT_SECRET_KEY = os.getenv("JWT_SECRET") JWT_TOKEN_LOCATION = ["cookies"] JWT_COOKIE_SECURE = os.getenv("FLASK_ENV") == "production" JWT_COOKIE_SAMESITE = "Lax"from app import create_appapp = create_app()
# Lancement# uv run flask --app wsgi run --debug# Prod : uv run gunicorn wsgi:appBlueprints — découper l’app
Section intitulée « Blueprints — découper l’app »Un blueprint = un sous-ensemble de routes regroupées. Permet d’organiser et d’éviter les fichiers monstrueux.
from flask import Blueprintbp = Blueprint('tasks', __name__)from . import routes # noqa (charge les routes)from flask import jsonify, requestfrom flask_jwt_extended import jwt_required, current_userfrom . import bpfrom .schemas import task_schema, tasks_schema, create_task_schemafrom app.extensions import dbfrom app.models import Task
@bp.get('/')@jwt_required()def list_tasks(): tasks = Task.query.filter_by(owner_id=current_user.id).all() return jsonify(tasks_schema.dump(tasks))
@bp.post('/')@jwt_required()def create_task(): payload = create_task_schema.load(request.json or {}) task = Task(owner_id=current_user.id, **payload) db.session.add(task) db.session.commit() return jsonify(task_schema.dump(task)), 201Models avec SQLAlchemy 2
Section intitulée « Models avec SQLAlchemy 2 »from datetime import datetimefrom app.extensions import db
class User(db.Model): id: int = db.Column(db.Integer, primary_key=True) email: str = db.Column(db.String(255), unique=True, nullable=False) name: str = db.Column(db.String(100), nullable=False) password_hash: str = db.Column(db.String(255), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Task(db.Model): id: int = db.Column(db.Integer, primary_key=True) owner_id: int = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) title: str = db.Column(db.String(200), nullable=False) description: str | None = db.Column(db.Text) done: bool = db.Column(db.Boolean, default=False, nullable=False) due_at = db.Column(db.DateTime, nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) owner = db.relationship('User', backref='tasks')Marshmallow — validation et sérialisation
Section intitulée « Marshmallow — validation et sérialisation »Marshmallow est le pendant Flask de Pydantic (FastAPI) ou Serializers (DRF).
from marshmallow import Schema, fields, validate
class CreateTaskSchema(Schema): title = fields.Str(required=True, validate=validate.Length(min=1, max=200)) description = fields.Str(validate=validate.Length(max=2000)) due_at = fields.DateTime()
class TaskSchema(Schema): id = fields.Int() title = fields.Str() description = fields.Str() done = fields.Bool() due_at = fields.DateTime() created_at = fields.DateTime()
create_task_schema = CreateTaskSchema()task_schema = TaskSchema()tasks_schema = TaskSchema(many=True)Auth avec Flask-JWT-Extended
Section intitulée « Auth avec Flask-JWT-Extended »from flask import jsonify, requestfrom flask_jwt_extended import ( create_access_token, set_access_cookies, unset_jwt_cookies, jwt_required)from werkzeug.security import check_password_hash, generate_password_hashfrom . import bpfrom app.extensions import dbfrom app.models import User
@bp.post('/register')def register(): data = request.json or {} if User.query.filter_by(email=data['email']).first(): return jsonify({'error': 'Email already used'}), 409 user = User( email=data['email'], name=data['name'], password_hash=generate_password_hash(data['password']), ) db.session.add(user) db.session.commit() return jsonify({'id': user.id, 'email': user.email, 'name': user.name}), 201
@bp.post('/login')def login(): data = request.json or {} user = User.query.filter_by(email=data['email']).first() if not user or not check_password_hash(user.password_hash, data['password']): return jsonify({'error': 'Invalid credentials'}), 401
token = create_access_token(identity=str(user.id)) response = jsonify({'id': user.id, 'email': user.email, 'name': user.name}) set_access_cookies(response, token) return response
@bp.post('/logout')@jwt_required()def logout(): response = jsonify(success=True) unset_jwt_cookies(response) return responseTests — client natif Flask
Section intitulée « Tests — client natif Flask »import pytestfrom app import create_appfrom app.extensions import db
@pytest.fixturedef app(): app = create_app() app.config.update(TESTING=True, SQLALCHEMY_DATABASE_URI="sqlite:///:memory:") with app.app_context(): db.create_all() yield app
@pytest.fixturedef client(app): return app.test_client()
# tests/test_auth.pydef test_register(client): response = client.post('/auth/register', json={ 'email': 'alice@example.com', 'name': 'Alice', 'password': 'password123', }) assert response.status_code == 201 assert response.json['email'] == 'alice@example.com'Quart — Flask en async
Section intitulée « Quart — Flask en async »Si tu as besoin de vraies perfs async tout en gardant l’API Flask, Quart est un drop-in replacement async. Mais en 2026, FastAPI est généralement le meilleur choix pour ça.
Déploiement
Section intitulée « Déploiement »FROM python:3.13-slimWORKDIR /appCOPY pyproject.toml uv.lock ./RUN pip install uv && uv sync --frozenCOPY . .CMD ["uv", "run", "gunicorn", "wsgi:app", "--workers", "4", "--bind", "0.0.0.0:8000"]Auto-évaluation
Section intitulée « Auto-évaluation »Pour aller plus loin
Section intitulée « Pour aller plus loin »- Flask Documentation — flask.palletsprojects.com
- Flask Mega-Tutorial — Miguel Grinberg (gratuit en ligne, la référence)
- Awesome Flask — github.com/humiaozuzu/awesome-flask
- Quart (Flask async) — quart.palletsprojects.com
Suite : FastAPI — pour le projet taskly-api.