Python può essere usato anche per creare siti web. Il modo migliore per farlo è utilizzare un framework dedicato che semplifica tutte le operazioni base, come Flask.
Flask è l'equivalente di Express per NodeJs.
La prima cosa da fare è installare Flask
pip install flask
Il classico sito di esempio "ciao mondo", basato su Flask, diventa:
# app.py
from flask import Flask

# creo la app Flask
app = Flask(__name__)

# definisco la prima route, home
@app.route("/")
def main():
    return "<html><head><title>Test flask</title></head><body><h1>ciao mondo</h1></body></html>"

# verifica se è il programma principale
# e manda in esecuzione il web server su http://localhost:5000
# in questo caso in debug, ovvero si riavvia ad ogni cambiamento dei file
if __name__ == "__main__":
    app.run(debug=True, port=5000)
dove, dopo aver importato il modulo Flask, creo l'applicazione app e, tramite l'attributo @app.route(percorso), definisco con quale url risponderà la specifica funzione (in questo caso main()).
La funzione associata ad una route deve restituire l'html che rappresenta la pagina.

Può essere testato con
py .\app.py

Ovviamente gestire una pagina web come stringa non è sicuramente il modo migliore per creare un sito.

Flask mette a disposizione una gestione basata su file di template html tramite il metodo render_template.
Per gestire i template deve essere creata una cartella templates in cui inserire tutti i template html, la struttura del progetto diventa:
  • templates
  • index.html
  • pagina1.html
  • app.py

Il file index.html conterrà questo html:
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{title}}</title>
  <!-- link rel="stylesheet" href="/css/style.css"-->
</head>
<body>
  <h1>ciao mondo</h1>
  <!-- script src="/app/app.js"></script-->
</body>
</html>
il file pagina1.html sarà identica con la differenza della scritta <h1>Pagina 1</h1>.
Ho utilizzato il placeholder racchiuso tra doppie parentesi graffe {{title}}, questo verrà sostituito in fase di render con il valore passato tramite il metodo render_template()
a questo punto posso modificare il codice ed utilizzare il template tramite il metodo render_template() passando il valore title:
# importo il modulo template
from flask import Flask, render_template
...
@app.route("/")
def main():
    return render_template("index.html", title="Test Falsk")
...
allo stesso modo in cui ho aggiunto la pagina di root / posso aggiungere anche altre pagine, ad esempio la url /abouts, che richiama la funzione pagina1:
...
@app.route("/abouts")
def pagina1():
    return render_template("pagina1.html", title="Altro titolo")
...
Posso fare un ulteriore ottimizzazione ai template, anziché riportare in ogni template le parti comuni, le sposto in un file che chiamo base.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{title}}</title>
  <!-- link rel="stylesheet" href="/css/style.css"-->
</head>
<body>
  <nav>
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/abouts">Pagina 1</a></li>
    </ul>
  </nav>
  {% block content %}{% endblock %}
  <!-- script src="/app/app.js"></script-->
</body>
</html>
Per chi conosce MVC in C# è l'equivalente della pagina di _Layout.cshtml.
La particolarità di questa pagina è l'uso del placeholder {% block content %}{% endblock %} che indica la posizione dove verrà inserito il contenuto delle singole pagine in fase di rendering.
Sempre per chi conosce MVC in C# è simile all'uso di @RenderBody() o @RenderSection(nomeSection) nella pagina di _Layout.cshtml.
Le singole pagine si semplificano e diventano:
<!-- index.html -->
{% extends "base.html" %}

{% block content %}
  <h1>ciao mondo</h1>
{% endblock %}
<!-- pagina1.html -->
{% extends "base.html" %}

{% block content %}
  <h1>Pagina 1</h1>
{% endblock %}
Si può notare l'uso del tag {% extends "base.html" %} per indicare che questa pagina userà, o meglio estenderà, la pagina base.html e il tag {% block content %} ... html ... {% endblock %} il cui contenuto verrà inserito nel template base nella posizione corrispondente all'omonimo tag content.

Il tag block ha questa sintassi, nella pagina base metto un tag nella forma:
{% block nome_del_blocco %}{% endblock %}
che può avere un contenuto di default, contenuto che verrà sovrascritto solo se il tag verrà usato in un template che estende base
{% block nome_del_blocco %}
... posso inserire qualunque codice html o altri tag ...
{% endblock %}
nel template, quando serve, uso la stessa sintassi, con lo stesso nome, per definire il blocco a cui voglio dare una diversa implementazione
{% block nome_del_blocco %}
... html ...
{% endblock %}
ovviamente posso avere più blocchi con nomi differenti.

Come ulteriore finezza aggiungo il template error404.html per customizzare gli errori di pagina non trovata
<html>
<body>
<h1>404 Pagina non trovata</h1>
</body>
</html>
e modifico il codice Python per reindirizzare tutte le richieste non risolte dalle route alla pagina di gestione errori
@app.errorhandler(404)
def page_not_found(error):
    return render_template('error404.html'), 404

Con la stessa tecnica, posso aggiungere quante pagine voglio tramite l'uso di @app.route(), lo schema è sempre questo:
@app.route(<url_percorso>)
def nome_funzione():
    return render_template(<nome_template>, <eventuali_parametri>)

Un sito però si compone anche di risorse statiche come immagini, file JavaScript, CSS, ecc...
Per utilizzare le risorse statiche deve essere creata l'applicazione Flask (variabile app) passando il percorso locale dove sono presenti le risorse statiche.
import os
from flask import Flask, render_template

# costruisco il percorso alla cartella "static" 
# basandomi sul path dell'applicazione corrente
static_file_dir = os.path.join(os.path.dirname(
    os.path.realpath(__file__)), "static")

# creo la app passando il percorso della cartella con le risorse statiche
app = Flask(__name__, static_url_path="/static", static_folder=static_file_dir)
Al template di progetto aggiungo la cartella static con le risorse necessarie:
  • static
  • logo.png
  • ...altri file o cartelle...
  • templates
  • base.html
  • index.html
  • ...altri template...
  • app.py
e modifico il template base per aggiungere un immagine statica
<!DOCTYPE html>
<html>
...
<body>
  <!-- immagine come risorsa statica -->
  <header><img src="/static/logo.png"></header>

  <nav>...
</body>
</html>

Finorà ho mostrato solo pagine html con contenuto statico.
Un sito reale si compone di pagine dinamiche, ovvero pochi template html valorizzati con dati presi da un database.
Quindi imposto l'applicazione con questa nuova struttura:
  • static
  • logo.png
  • ...altri file o cartelle...
  • templates
  • base.html
  • error404.html
  • page.html
  • app.py
  • mock.py

Per prima cosa, creo il modulo mock.py che simulerà il database:
# mock.py

# definisco 3 pagine, simulo il db con una collection
db = [
    {"id": 0, "title": "Home", "body": "By <a href='https://www.sgart.it'><b>sgart.it</b></a>"},
    {"id": 1, "title": "Pagina 1", "body": "corpo della <b>pagina 1</b>"},
    {"id": 2, "title": "Info", "body": "Test Flask"},
]

# metodo per recuperare la pagina singola
def get(id):
    for item in db:
        if item["id"] == id:
            print(item)
            return item
    return None

# metodo per recuperare il menu in base alle pagine presenti nel db
def menu():
    items = []
    for item in db:
        items.append({"id": item["id"], "title": item["title"]})
    return items
In Python l'equivalente delle keyword null di altri linguaggi è None.
questo modulo sarà usato da app.py con le seguenti modifiche:
# app.py
import os
...
# importo il modulo mock che simula il database
import mock as db

...

# definisco un metodo che si occupa di caricare la pagina dal DB
# se non la trova mostra la pagina di errore 404
def get_page(id):
    page = db.get(id)
    if page == None:
        # se non trovo la pagina errore 404
        return page_not_found(None)
    # recupero il menu
    menu = db.menu()
    # faccio il render
    return render_template("page.html", menu=menu, page=page) 

# definisco la prima route, home, caricherà la pagina con id=0
@app.route("/")
def main():
    return get_page(0)
  
# aggiungo una nuova route per caricare le altre pagine nella forma /pagina/2
@app.route("/pagina/<int:id>")
def pagina(id):
    return get_page(id)
  
...
In questo caso uso una route con parametri nella forma @app.route("/nome_url/<tipo_dato:nome_parametro>"), che corrisponde a una url tipo: /pagina/2. Dove il valore 2 corrisponde all'id della pagina nel database.
Ho modificato il metodo render_template per passare sia la collection con i valori per il menu, sia il dictionary con i dati della pagina (page), quindi diventa return render_template("page.html", menu=menu, page=page).

Adesso non rimane che ridefinire i template per generare in automatico il menu e la pagina.
La pagina base.html diventa:
...
  <style>li.selected a { font-weight: bold; }</style>
</head>
<body>
  ...
  <nav>
    <ul>
      <!-- genero il menu usando un ciclo for -->
      {% for item in menu %}
        <!-- determino se il menu corrisponde alla pagina corrente, se si lo evidenzio-->
        <li class="{{ 'selected' if item['id'] == page['id'] else '' }}">
          {% if item["id"] == 0 %}
            <!-- se è la home page, semplifico il link -->
            <a href="/">{{item['title']}}</a></li>
          {% else %}
            <a href="/pagina/{{item['id']}}">{{item['title']}}</a>
          {% endif %}
        </li>
      {% endfor %}
    </ul>
  </nav>
  <div class="content">
    {% block content %}{% endblock %}
  </div>
</body>
</html>
in questo template ho introdotto alcune cose nuove:
  • il ciclo for tramite la sintassi: {% for <variabile> in <collection> %} ...html... {% endfor %}
  • il costrutto if tramite la sinstassi: {% if <condizione> %} ...html condizione vera... {% else %} ...html condizione falsa... {% endif %}
  • il già citato {% block content %}{% endblock %} usato come placeholder per il contenuto delle singole pagine
non rimane che definire il template della singola pagina, page.html:
<!-- page.html -->
{% extends "base.html" %}
 
{% block content %}
  <h1>{{page["title"]}}</h1>
  <div class="body">
    {# commento: uso "safe" per evitare l'escape dei codici html #}
    {{page["body"] | safe}}
  </div>
{% endblock %}
Dato che il il body della pagina continene anche del codice html, non è sufficiente il tag nella forma {{ valore }}, Questa sintassi fa un render sicuro facendo l'escape di tutti i tag html.
Se voglio che l'html venga renderizzato correttamente, senza modifiche, devo usare il filtro safe nella forma {{ valore | safe }}

Ricapitolando un progetto Python con Flask prevede:
  • l'import dei muduli os e flask
  • la creazione di un app app = Flask(__name__, static_url_path="/static", static_folder=static_file_dir) con il supporto per i file statici
  • la definizione di una o più route @app.route(percorso) con eventuali parametri
  • la creazione di una cartella templates con file html, solitamente un base.html e uno o più template di pagina
nei template posso avere i seguenti tag:
  • {{ valore }}: inserisce il valore di una variabile nel template ( con la variante {{ valore | safe }} )
  • {{% ... %}}: istruzioni, tipo: for, if, block, extends, assegnamento variabili, ecc...
  • {{# ... #}}: inserisce un commento in pagina che non verrà renderizzato nell'html

Se ho la necessità di inserire delle parentesi graffe senza che vengano interpretate come tag, posso usare {% raw %}:
{% raw %}
   <p>
      Esempio di escape: con il tag {% raw %} posso usare le { parentesi graffe }
      senza che vengano processate dal parser del template
   </p>
{% endraw %}

L'esempio finale completo può essere trovato su GitHub - Esempio Flask in Python
Esempio finaleEsempio finale

Questa è solo una veloce panoramica di Flask, per approfondimenti vedi la documentazione ufficiale.