Flask: Building Lightweight Web Applications in Python
Flask is a micro-framework for building web applications in Python: it provides the bare essentials (routing, templates, request handling) and lets you choose the rest (database, authentication, forms). This minimalist philosophy makes it an excellent entry point for learning web development in Python, while remaining powerful enough for production applications. This guide takes you from installation all the way to deployment.
Installation and a Minimal Application
Create a virtual environment, then install Flask:
# Création et activation de l'environnement virtuel
python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activate
# Installation
pip install flask
A complete Flask application fits in just a few lines. Create a file named app.py:
from flask import Flask
# Création de l'application
app = Flask(__name__)
# Association d'une URL à une fonction (une "vue")
@app.route('/')
def accueil():
return "Bonjour, Flask !"
if __name__ == '__main__':
# debug=True : rechargement automatique et messages d'erreur détaillés
app.run(debug=True)
Start the development server:
python app.py
# * Running on http://127.0.0.1:5000
Open http://127.0.0.1:5000 in your browser: the page displays “Bonjour, Flask !”. Never use debug=True in production: the built-in debugger allows arbitrary code execution.
Routes and HTTP Methods
The @app.route decorator maps a URL to a function. By default, a route only responds to GET requests; the methods parameter allows other HTTP verbs:
from flask import Flask, request
app = Flask(__name__)
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
# Traitement de l'envoi du formulaire
return "Message reçu"
# Affichage du formulaire (GET)
return "Formulaire de contact"
| Method | Typical use |
|---|---|
| GET | Read a resource, display a page |
| POST | Create a resource, submit a form |
| PUT | Replace an existing resource |
| PATCH | Partially update a resource |
| DELETE | Delete a resource |
URL Parameters
Variable segments of a URL are declared between angle brackets and passed as arguments to the function:
# Paramètre chaîne (par défaut)
@app.route('/utilisateur/<nom>')
def profil(nom):
return f"Profil de {nom}"
# Paramètre entier : /article/42 fonctionne, /article/abc renvoie 404
@app.route('/article/<int:article_id>')
def article(article_id):
return f"Article numéro {article_id}"
# Paramètre chemin : accepte les barres obliques
@app.route('/fichiers/<path:chemin>')
def fichier(chemin):
return f"Chemin demandé : {chemin}"
Available converters: string (default), int, float, path, uuid.
Query parameters (?tri=date&page=2) are read with request.args:
@app.route('/recherche')
def recherche():
terme = request.args.get('q', '') # valeur par défaut : ''
page = request.args.get('page', 1, type=int) # conversion automatique
return f"Recherche de « {terme} », page {page}"
Jinja2 Templates
Returning HTML inside Python strings quickly becomes unreadable. Flask includes the Jinja2 template engine: HTML files live in a templates/ folder.
Project structure:
mon_projet/
├── app.py
├── templates/
│ ├── base.html
│ └── index.html
└── static/
└── style.css
Template Inheritance
The base template defines the shared structure; pages fill in the blocks:
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{% block titre %}Mon Site{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('accueil') }}">Accueil</a>
</nav>
<main>
{% block contenu %}{% endblock %}
</main>
</body>
</html>
<!-- templates/index.html -->
{% extends "base.html" %}
{% block titre %}Accueil - Mon Site{% endblock %}
{% block contenu %}
<h1>Bienvenue, {{ nom }} !</h1>
{% if articles %}
<ul>
{% for article in articles %}
<li>{{ article.titre }} ({{ article.date }})</li>
{% endfor %}
</ul>
{% else %}
<p>Aucun article pour le moment.</p>
{% endif %}
{% endblock %}
On the Python side, render_template loads the template and passes variables to it:
from flask import render_template
@app.route('/')
def accueil():
articles = [
{"titre": "Premier article", "date": "2026-09-01"},
{"titre": "Deuxième article", "date": "2026-09-10"},
]
return render_template('index.html', nom="Alice", articles=articles)
Jinja2 automatically escapes HTML in variables, which protects against XSS attacks.
Static Files
Stylesheets, scripts, and images go in the static/ folder. The url_for function generates their URLs:
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
Always use url_for rather than hard-coded paths: URLs remain correct even if the application is deployed under a prefix.
Forms and the request Object
Data from a form submitted via POST is read with request.form:
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/inscription', methods=['GET', 'POST'])
def inscription():
if request.method == 'POST':
nom = request.form.get('nom', '').strip()
email = request.form.get('email', '').strip()
# Validation minimale
if not nom or not email:
return render_template('inscription.html',
erreur="Tous les champs sont obligatoires")
# Ici : enregistrement en base de données
return f"Bienvenue, {nom} !"
return render_template('inscription.html')
<!-- templates/inscription.html -->
{% extends "base.html" %}
{% block contenu %}
{% if erreur %}<p class="erreur">{{ erreur }}</p>{% endif %}
<form method="post">
<input type="text" name="nom" placeholder="Votre nom">
<input type="email" name="email" placeholder="Votre email">
<button type="submit">S'inscrire</button>
</form>
{% endblock %}
For real-world projects, the Flask-WTF library adds validation and CSRF protection.
Redirects and Flash Messages
After a successful POST, redirect the user (the “Post/Redirect/Get” pattern) and display a temporary message with flash:
from flask import Flask, flash, redirect, url_for, render_template
app = Flask(__name__)
app.secret_key = "changez-moi-en-production" # requis pour flash et les sessions
@app.route('/enregistrer', methods=['POST'])
def enregistrer():
# ... traitement ...
flash("Enregistrement réussi !", "succes")
return redirect(url_for('accueil'))
<!-- Dans base.html, avant le bloc contenu -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% for categorie, message in messages %}
<div class="flash flash-{{ categorie }}">{{ message }}</div>
{% endfor %}
{% endwith %}
JSON APIs with jsonify
Flask is well suited to REST APIs. The jsonify function serializes a dictionary into a JSON response with the correct Content-Type header:
from flask import Flask, jsonify, request
app = Flask(__name__)
TACHES = [
{"id": 1, "titre": "Apprendre Flask", "faite": True},
{"id": 2, "titre": "Créer une API", "faite": False},
]
@app.route('/api/taches', methods=['GET'])
def lister_taches():
return jsonify(TACHES)
@app.route('/api/taches/<int:tache_id>', methods=['GET'])
def detail_tache(tache_id):
tache = next((t for t in TACHES if t["id"] == tache_id), None)
if tache is None:
return jsonify({"erreur": "Tâche introuvable"}), 404
return jsonify(tache)
@app.route('/api/taches', methods=['POST'])
def creer_tache():
donnees = request.get_json() # corps JSON de la requête
if not donnees or "titre" not in donnees:
return jsonify({"erreur": "Le champ 'titre' est requis"}), 400
nouvelle = {"id": len(TACHES) + 1, "titre": donnees["titre"], "faite": False}
TACHES.append(nouvelle)
return jsonify(nouvelle), 201
Quick test with curl:
curl http://127.0.0.1:5000/api/taches
# [{"faite": true, "id": 1, "titre": "Apprendre Flask"}, ...]
curl -X POST http://127.0.0.1:5000/api/taches \
-H "Content-Type: application/json" \
-d '{"titre": "Déployer"}'
# {"faite": false, "id": 3, "titre": "Déployer"}
Structuring with Blueprints
As the application grows, a single file becomes unmanageable. Blueprints group routes by functional area:
# blog/routes.py
from flask import Blueprint, render_template
blog_bp = Blueprint('blog', __name__, url_prefix='/blog')
@blog_bp.route('/')
def liste_articles():
return render_template('blog/liste.html')
@blog_bp.route('/<int:article_id>')
def detail(article_id):
return render_template('blog/detail.html', article_id=article_id)
# app.py
from flask import Flask
from blog.routes import blog_bp
app = Flask(__name__)
app.register_blueprint(blog_bp)
# Les routes /blog/ et /blog/<id> sont maintenant actives
Typical structure of an organized project:
mon_projet/
├── app.py
├── blog/
│ └── routes.py
├── auth/
│ └── routes.py
├── templates/
└── static/
Configuration and Environment Variables
Never put secrets (keys, database passwords) in your code. Load them from the environment:
import os
from flask import Flask
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-uniquement')
app.config['DATABASE_URL'] = os.environ.get('DATABASE_URL')
app.config['DEBUG'] = os.environ.get('FLASK_DEBUG', '0') == '1'
In development, the python-dotenv package automatically loads a .env file (which should be excluded from your Git repository via .gitignore):
# .env
SECRET_KEY=une-longue-chaine-aleatoire
DATABASE_URL=postgresql://user:pass@localhost/mabase
FLASK_DEBUG=1
Deployment: Gunicorn and nginx
Flask’s built-in server is single-process and insecure: it is intended for development only. In production, use a WSGI server such as Gunicorn behind an nginx web server:
pip install gunicorn
# 4 processus de travail, à l'écoute sur le port 8000
gunicorn --workers 4 --bind 127.0.0.1:8000 app:app
nginx acts as a reverse proxy: it receives HTTPS traffic, serves static files directly, and forwards everything else to Gunicorn:
server {
listen 80;
server_name exemple.com;
location /static/ {
alias /var/www/mon_projet/static/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Platforms such as Render, Railway, or Fly.io automate this setup if you’d rather not manage a server.
Flask vs. Django
| Criterion | Flask | Django |
|---|---|---|
| Philosophy | Micro-framework, minimal | Full-featured framework, “batteries included” |
| Learning curve | Gentle, you learn as your needs grow | Steeper, many concepts upfront |
| ORM | Your choice (SQLAlchemy most often) | Built-in Django ORM |
| Administration | Build it yourself or use an extension | Automatically generated admin interface |
| Authentication | Extension (Flask-Login) | Built in |
| Project structure | Free-form | Enforced and conventional |
| Ideal for | APIs, microservices, small sites, prototypes | Full applications with a back office |
Both frameworks are mature and used in production at large scale. Choose Flask for its lightness and flexibility, and Django when you want the maximum number of ready-to-use features.
Best Practices
- Always use a virtual environment and a
requirements.txtfile (pip freeze > requirements.txt). - Disable debug mode in production: it is a major security vulnerability.
- Generate URLs with
url_forrather than hard-coding them. - Store secrets in environment variables, never in the source code.
- Structure with blueprints as soon as the application grows beyond a few routes.
- Return the correct HTTP status codes in your APIs: 201 for a creation, 400 for an invalid request, 404 for a missing resource.
- Validate all user input on the server side, even if the form already validates it on the client side.
Conclusion
Flask perfectly illustrates the Python philosophy: simple to get started, extensible as you grow. You now know how to create routes, generate pages with Jinja2, process forms, expose a JSON API, organize your code with blueprints, and deploy with Gunicorn and nginx. The logical next steps: connect a database with Flask-SQLAlchemy, handle authentication with Flask-Login, and secure your forms with Flask-WTF.
To go further, check out the official Flask documentation, which is remarkably clear and comprehensive.
About InSkillCoach
Training and technology expert
Coach specializing in advanced technologies and AI, backed by GNeurone Inc.
Certifications:
- AWS Certified Solutions Architect – Professional
- Google Cloud certifications
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comments
Comments are powered by GitHub Discussions
Sign in with GitHub to join the discussion