0%
Flask: Building Lightweight Web Applications in Python

Flask: Building Lightweight Web Applications in Python

Discover Flask for building lightweight web applications in Python: routes, Jinja2 templates, forms, JSON APIs, blueprints, and production deployment.

I

InSkillCoach

· min

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"
MethodTypical use
GETRead a resource, display a page
POSTCreate a resource, submit a form
PUTReplace an existing resource
PATCHPartially update a resource
DELETEDelete 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

CriterionFlaskDjango
PhilosophyMicro-framework, minimalFull-featured framework, “batteries included”
Learning curveGentle, you learn as your needs growSteeper, many concepts upfront
ORMYour choice (SQLAlchemy most often)Built-in Django ORM
AdministrationBuild it yourself or use an extensionAutomatically generated admin interface
AuthenticationExtension (Flask-Login)Built in
Project structureFree-formEnforced and conventional
Ideal forAPIs, microservices, small sites, prototypesFull 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.txt file (pip freeze > requirements.txt).
  • Disable debug mode in production: it is a major security vulnerability.
  • Generate URLs with url_for rather 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.

InSkillCoach

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+
1.1k
330

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!