Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Laboratorio di Reti

Le dispense per il corso di reti del prof



Teoria delle Reti

In questo gruppo trovate le dispense sulla teoria delle Reti: storia, protocolli, le informazioni di base.

Python: Net Programming

La programmazione di rete in Python: dalla gestione di base dei socket alle librerie moderne per la gestione delle connessioni di rete.

Linguaggi del Web

I mattoni alla base della realizzazione dei siti web: i linguaggi che costituiscono il www.

Flask

Corso di programmazione web con Flask. La programmazione web lato server, gestita tramite un framework semplice e flessibile.

Teoria e Pratica delle Reti

Nelle dispense di laboratorio come questa, teoria e pratica si intrecciano continuamente. La teoria delle reti aiuta a capire come funziona il mondo di oggi, ormai completamente immerso nella rete Internet; la programmazione di rete non è un argomento che si può affontare come la programmazione classica: ti spiegano il concetto, guardi un tutorial, etc… convivono in essa due anime distinte ma che devono collaborare attivamente. Da un lato la conoscenza dei concetti teorici alla base delle reti: socket, protocolli, il meccanismo request-reply, etc… dell’altra la conoscenza delle classi e degli artifici del linguaggio (Python, nel nostro contesto) per implementare i concetti teorici.

Sono come due facce della stessa medaglia: senza conoscere la teoria non si riesce a fare nulla con determinati strumenti della programmazione; d’altro canto, se si studiano certi concetti senza metterli in pratica è impossibile raggiungere un livello di consapevolezza e di comprensione sufficiente.

In realtà gli aspetti da affrontare dovrebbero essere 3 per ogni argomento:

  1. la base teorica: la conoscenza dei concetti alla base di un discorso (es: un server web, una rete di un ufficio, la condivisione del wifi, etc…)
  2. il laboratorio di reti: l’installazione e la configurazione degli strumenti esistenti per l’implementazione pratica.
  3. la programmazione di rete: la scrittura di codice per implementare (almeno a livello base)i concetti teorici.

Ci proveremo!

Per almeno qualche argomento… ci proveremo! 😉

La Suite Internet

La Internet Protocol Suite è un insieme di protocolli di rete che implementa la pila di protocolli su cui funziona Internet.

Se la cercate in rete o nella letteratura attuale, la troverete nominata in vari modi: Internet Protocol Suite, o più semplicemente Suite Internet, o addirittura con il nomignolo TCP/IP, che deriva dalla grande importanza storica dei suoi due protocolli più rappresentativi: TCP e IP.

Note

Protocolli (di rete) e suite(s)

Un protocollo è un insieme di regole utilizzate per favorire la comunicazione tra due o più entità.

Ovviamente nel caso specifico di un protocollo di rete, queste regole andranno a definire le modalità di interazioni fra due o più dispositivi.

Uno stack (o una suite) di protocolli è un insieme di protocolli che collaborano fra loro per ottenere uno scopo comune.

La necessità di implementare realmente i livelli descritti ha portato ad una semplificazione di alcune parti del modello OSI, come si desume dalla figura seguente:

Confronto fra OSI e TCP/IP

Il livello superiore, la cui implementazione è affidata al software, si occupa di stabilire le regole che permettono ai dati utente di essere trasportati in maniera organizzata: si passa quindi dai dati (chat, siti web, files, etc…) ai blocchi, insiemi organizzati di dati.

Il livello di trasporto, implementato a livello di Sistema Operativo, si occupa prevalentemente di individuare il mittente e il destinatario della comunicazione, ovvero il punto di partenza dei blocchi che arrivano dal livello superiore e il punto ove questi devono arrivare.

Il livello di rete, implementato nel firmware (driver, router, etc), si occupa di due compiti fondamentali: preparare i pacchetti dati che saranno inviati, completi delle informazioni arrivate dai livelli precedenti, e stabilire il percorso che i pacchetti devono compiere per arrivare dal mittente al destinatario.

Infine il livello inferiore, implementato a livello hardware, si occupa di trasformare i pacchetti logici in segnali fisici.

Warning

La storia di un pacchetto

(qui ci andrà un disegno fatto dal prof 😱)
Un disegno che spiega il passaggio di un pacchetto dati attraverso i livelli della suite Internet

Semplice e schematico ☺️

URL: Uniform Resource Locator

Un URL è una sequenza di caratteri compresi nel codice ASCII che identifica univocamente l’indirizzo di una risorsa in una rete.

L’idea di base dell’invenzione degli URL è quella di identificare ogni risorsa con un codice alfanumerico facile da ricordare (es: google.it, facebook.com) invece di avere informazioni dettagliate sull’indirizzo IP, il socket e il percorso relativo della risorsa.

La struttura completa di ogni URL, definita nel RFC 3986, è la seguente:

scheme : // ( user ( : pass ) @ ) host ( :port ) ( /path ) ( ? query ) ( # fragment )

Nello schema sopraelencato le parentesi tonde indicano una informazione opzionale che, se presente, utilizza il separatore indicato. Ad esempio, se la query è presente nell’URL è sempre preceduta dal simbolo “?”. L’host è sempre presente. Pass e port, che sono entrambe precedute dal simbolo “:” si distinguono perché una sta prima del simbolo “@” (e dell’host) e una dopo.

Ognuna delle parti citate ha un significato identificativo per un concetto derivato dalla struttura dei protocolli che un URL può identificare:

SCHEME (o PROTOCOL)
Descrive il protocollo da utilizzare per l’accesso ai server. Quando questa informazione è assente, l’URL si dice relativo e sta al software che lo utilizza scegliere (con un po’di fortuna) il protocollo da utilizzare.

USERINFO (= USER : PASS)
Specificano l’autenticazione per l’accesso alla risorsa. Nel protocollo HTTP, questa modalità di autenticazione è ritenuta rischiosa per motivi di sicurezza e di phishing, mentre ad esempio, gli account di posta sono scritti tipicamente come USER@HOST.

HOST
rappresenta l’informazione che sarà passata al server DNS per l’identificazione dell’indirizzo IP del dispositivo in cui risiede la risorsa. É l’informazione più importante contenuta nell’URL.

PORT
è un numero compreso fra 1 e 65535 che serve per distinguere le connessioni dal punto di vista logico. Molti protocolli utilizzano una porta fissa per identificare il server, in modo tale che questo numero venga spesso sottinteso.

PATH
percorso relativo nel file system del server per raggiungere la risorsa.

QUERY
Informazioni opzionali che devono essere elaborate dalla risorsa.

FRAGMENT
identifica una parte specifica della risorsa.



Vediamo insieme qualche esempio di URL per cercare di capire meglio!

  1. http://www.liceodavincijesi.edu.it/area-studenti/

Qui troviamo lo scheme (http) seguito dall’host (liceodavincijesi.edu.it) e dal path (area-studenti). Non sono presenti altre componenti.

  1. http://www.google.it/search?q=ciao

Qui troviamo scheme (http), host (www.google.it), path (search) e query (q=ciao)

  1. mailto:andrea@liceodavincijesi.edu.it

Qui troviamo scheme (mailto), user (andrea) e host (liceodavincijesi.edu.it). Lo schema mailto presenta una piccola eccezione alla regola in quanto non è seguito dal doppio slash (//).

  1. http://www.facebook.com@www.cicciobello.com/tihofregato/

Se non hai cercato su internet il significato del termine phishing, in questo esempio trovi uno dei più vecchi trucchi di mascheramento dell’identità: qual è l’host qui? Su quale sito sei?



HTTP

HTTP sta per Hyper Text Transfer Protocol ed è un protocollo di trasferimento di ipertesti del livello superiore della suite Internet.

E’ stato inventato nel 1989 da Tim Berners-Lee insieme a tutto ciò che compone il World Wide Web: il protocollo HTTP, il linguaggio HTML, il primo browser mosaic e il primo server web, httpd.

Note

Commento del prof

HTTP è una invenzione rivoluzionaria nel mondo di oggi, paragonabile a quella della scrittura. E’ la tecnologia che ha spinto Internet a diventare quello che è oggi.
Ha cambiato il mondo più dell’auto, ha diffuso idee, comportamenti e indicato acquisti più della carta.
E’ il motivo per cui oggi tutti abbiamo un telefono in tasca.

HTTP si sviluppa a partire dal 1989 ad oggi secondo varie innovazioni e cambiamenti. Vediamo prima una tabella riassuntiva delle cose che più di tutti hanno modificato il protocollo. Poi vedremo quelle che sono le sue caratteristiche principali.

Riepilogo delle principali versioni HTTP

VersioneAnnoRFC
HTTP/0.91991RFC 1945
HTTP/1.11999RFC 2616
HTTP/22015RFC 7540
HTTP/32022RFC 9114

HTTP 0

HTTP 0.9 è l’ultima delle versioni originali del protocollo. Permette il trasferimento solo di testo semplice, quindi di file HTML statici senza immagini.


HTTP 1

HTTP 1 è la versione che fa la differenza per HTTP: permette l’invio di dati in formato binario, oltre al testo, grazie alla tecnica (importata dai protocolli di mail) dei MIME types da inserire nel nuovo header della risposta, il Content-Type.

HTTP 1.1 in particolare è importante perché rende obbligatorio l’uso dell’header Host nella richiesta. Questo permette ad un unico server di servire più siti web, identificati tramite il nome del dominio lì presente: questa novità introduce una nuova tipologia di azienda: le web farm and hosting companies.


HTTP 2

Il difetto principale di HTTP 1 era la lentezza: le risposte erano inviate in modo sequenziale, una dopo l’altra, senza possibilità di inviare più dati contemporaneamente. L’obiettivo principale per HTTP 2 è la velocità!

HTTP 2 introduce il multiplexing, ovvero la possibilità di inviare più richieste contemporaneamente e introduce la compressione obbligatoria dei dati, così da ridurre la dimensione totale da inviare. Introduce inoltre le risposte push: mentre su HTTP 1 le richieste/risposte sono sempre a coppie, in HTTP 2 ad una richiesta (es: quella per il sito adjam.org) possono corrispondere decine di risposte contemporaneamente (ovvero tutte quelle relative al sito stesso).


HTTP 3

HTTP 3 si occupa invece del problema della sicurezza: introduce la crittografia in HTTP in maniera obbligatoria, non opzionale come su HTTP 1 e HTTP 2.

Inoltre si preoccupa di risolvere il problema del carico aggiuntivo di lavoro che questa sicurezza comporta: viene introdotto il protocollo QUIC, che si basa su UDP, come protocollo di trasporto più efficiente con la crittografia di TLS 1.3 integrata.

Caratteristiche principali

Il protocollo HTTP è un protocollo basato sulla tipologia client/server e sul meccanismo, comunissimo nelle comunicazioni di rete di alto livello, di request/reply (richiesta/risposta): il client esegue una richiesta ed il server restituisce la risposta e poi la connessione viene chiusa (protocollo stateless, senza stato, cioè che non mantiene la connessione attiva).

Il protocollo HTTP si appoggia a livello di trasporto sul protocollo TCP e utilizza lato server la porta 80, mentre la versione HTTPS utilizza la porta 443. La versione 3 del protocollo HTTP si basa su QUIC e utilizza la porta 443 avendo crittografia integrata.

Richiesta e risposta sono comunissimi file di testo: questo serve HTTP facile da implementare, gestire, studiare e “controllare”. Ogni coppia request/reply corrisponde ad un unico file da scambiare fra client e server: questo significa che se un sito è costituito da una pagina con 20 immagini saranno necessarie 20+1 risposte per inviarle tutte! Nel caso di HTTP 1 vi erano 21 coppie richiesta/risposta. Da HTTP 2 in avanti, nel caso di un sito ben configurato sul server, è sufficiente una sola richiesta per ottenere tutte le risorse necessarie.

Richieste HTTP

Il messaggio di richiesta HHTP è composto di tre parti:

  1. Riga di richiesta (request line)
  2. Sezione Header (informazioni aggiuntive)
  3. Body (corpo del messaggio)

La riga di richiesta è composta a sua volta da tre informazioni:

  • la versione del protocollo: es: HTTP/1.1.
  • il metodo di richiesta; obbligatorio conoscere almeno i metodi GET, POST, HEAD, PUT.
  • L’URL; l’informazione che identifica l’oggetto della richiesta (ad esempio la pagina del sito che si vuole vedere).

I metodi HTTP indicano le cose che il protocollo può fare e identificano dunque le azioni da eseguire.

Il metodo GET è il più utilizzato e serve a richiedere una pagina specifica. Si attende la risorsa corrispondente all’URL specificato pensando che arrivi nel body della risposta associata alla richiesta.

Il metodo HEAD è identifico al metodo GET, se non per una importante differenza: il body della risposta associata ad una richiesta HEAD è sempre vuoto! Questo tipo di richiesta è utilizzato per ottenere solo l’header della risposta, senza il body.

Il metodo POST è utilizzato per inviare dati dal client al server; i dati saranno inseriti nel body della richiesta . La risposta associata a una richiesta POST può contenere una conferma di ricezione, una pagina di accesso (nel caso ad esempio che i dati inviati siano delle credenziali di accesso), o un errore.

Il metodo PUT è utilizzato per l’upload di file dal cliente al server. Anche qui, il file da caricare andrà nel body della richiesta e la risposta sarà una semplice conferma di ricezione.

Gli header di richiesta più comuni sono:

  • Host: Nome del server a cui si riferisce l’URI. È obbligatorio nelle richieste conformi HTTP/1.1 perché permette l’uso dei virtual host basati sui nomi.
  • User-Agent: Identificazione del tipo di client: tipo browser, produttore, versione…
  • Referer: indica l’URL che ha “prodotto” la richiesta corrente.
  • Accept-Language: lista delle lingue che l’utente è in grado di comprendere

Il body contiene il corpo del messaggio. Il suo contenuto dipende dal metodo utilizzato:

  • è vuoto con i metodi GET e HEAD.
  • contiene i dati da inviare con il metodo POST
  • il file di cui fare l’upload con il metodo PUT
  • etc…
Esempio di richiesta HTTP 1.1
HTTP/1.1 GET /
Host: www.adjam.org
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 ...
Accept-Language: it
... (body) ...

Risposte HTTP

Il messaggio di risposta è anch’esso composto da tre parti:

  1. Riga di stato (status-line)
  2. Sezione header
  3. Body (contenuto della risposta)

La riga di stato riporta (anche qui) tre informazioni:

  • la versione HTTP (es. HTTP/1.1)
  • un codice numerico a tre cifre che identifica il tipo di risposta. I numeri vanno da 100 a 599. Le centinaia identificano il tipo di risposta
  • una descrizione testuale della risposta (es. OK, Not Found, Internal Server Error)

Vediamo le diverse tipologie:

  • 1xx: Informational (messaggi informativi, non di errore)
  • 2xx: Success (la richiesta è stata soddisfatta)
  • 3xx: Redirection (la risorsa non è presente, ma si hanno informazioni su come raggiungerla)
  • 4xx: Client error (la richiesta non può essere soddisfatta perché sbagliata)
  • 5xx: Server error (la richiesta non può essere soddisfatta per un problema interno del server)

Note

Ovviamente non tutti i 500 numeri sono utilizzati nella realtà!
Wikipedia riporta un elenco aggiornato delle status line esistenti (http://en.wikipedia.org/wiki/List_of_HTTP_status_codes).

Gli header della risposta più comuni sono:

  • Server: Indica tipo e versione del server
  • Content-Type: Indica il tipo di contenuto restituito (tipo MIME).
  • Content-encoding: Indica se il contenuto è stato compresso oppure no.
  • Content-length: Indica la lunghezza in byte del body

Il body della risposta è tipicamente il posto dove troviamo le informazioni trasportate: ad esempio, in caso di una richiesta GET, il body della risposta contiene il file da trasportare al client.

Esempio di risposta HTTP
HTTP/1.1 200 OK
Server: Apache
Content-Encoding: gzip
Content-Length: 1122
Content-Type: text/html
... (seguita dal body coi dati richiesti) ...

Suite Internet: Livello di Trasporto

Il livello di trasporto gestisce la conversazione tra mittente e destinatario, nascondendo tutti i dettagli relativi al trasporto delle informazioni lungo la rete.

I compiti del livello si possono inizialmente riassumere come nel disegno sottostante: prende i dati dal livello applicazione, li divide in pacchetti, vi aggiunge un’intestazione e li passa al sottostante livello di rete.

Segmentazione

La Suite Internet offre a livello di trasporto due protocolli:

  • TCP (Transmission Control Protocol) connesso e affidabile
  • UDP (User Datagram Protocol) non connesso e non affidabile

A livello di trasporto, i termini connessione e affidabilità significano:

  1. connessione:

    Un servizio si dice connesso (a livello di trasporto) quando si preoccupa di stabilire una comunicazione con il destinatario preventiva all’invio reale dei dati.

  2. Affidabilità:

    Un servizio si dice affidabile (a livello di trasporto) quando si preoccupa di rinviare al destinatario ogni pacchetto che non gli è arrivato (o che gli è arrivato corrotto).

Un servizio di trasporto connesso e affidabile si preoccupa di stabilire una comunicazione preventiva con il destinatario. Se questi è disponibile, invia i pacchetti numerandoli alla partenza e riordinandoli all’arrivo; in caso di pacchetti corrotti o mancanti si preoccupa di richiedere al mittente un nuovo invio di dati, assicurando un arrivo completo degli stessi, oppure una dichiarata impossibilità a ricevere i dati.

Un servizio di trasporto non connesso e non affidabile inizia subito l’invio dei pacchetti nell’ordine in cui essi sono arrivati dal livello precedente. All’arrivo scarta semplicemente i pacchetti corrotti, senza richiedere alcun reinvio, toglie l’intestazione del livello di trasporto e passa il dato al livello superiore.

Note

“Perché due protocolli?”

TCP è connesso e affidabile.

Alcune applicazioni, come ad esempio quelle per inviare mail o files, hanno bisogno di essere sicure che i loro dati arrivino tutti a destinazione e possono accettare piccoli rallentamenti nel trasporto.

Queste applicazioni scelgono TCP.


UDP è non connesso e non affidabile.

Alcune applicazioni, ad esempio quelle di video e audio streaming, gestiscono i dati in maniera che anche con piccoli “buchi” o “errori” essi siano fruibili; preferiscono “scartare” alcune parti del dato pur di proseguire nella trasmissione e non necessitano di un riordinamento.

Queste applicazioni scelgono UDP.

Sockets e Porte

Il protocollo di trasporto per suddividere il traffico in entrata e uscita fra le varie applicazioni che ne richiedono utilizza il meccanismo delle porte logiche.

Le porte sono il mezzo essenziale che permette ai protocolli di trasporto di gestire flussi multipli di dati attraverso una unica connessione fisica alla rete.

Per fare un paragone con la vita reale immaginiamo di spedire una lettera ad un amico. Se il destinatario vivesse in una abitazione indipendente e ne fosse l’unico inquilino, sulla busta sarebbe sufficiente indicare il suo indirizzo.

Questa situazione è però piuttosto inusuale in quanto, più probabilmente, egli vivrà in un palazzo con altri condomini oppure condividerà l’abitazione con altri parenti. Ognuno di questi può ricevere posta al medesimo indirizzo e quindi per individuare univocamente il destinatario sarà necessario specificare anche il suo nome completo. Allo stesso modo indicheremo sulla busta le informazioni riguardanti il mittente per poter ricevere una risposta.

Una cosa analoga avviene per le comunicazioni in rete attraverso la suite TCP/IP. Ogni macchina sarà individuata sulla rete da un indirizzo IP ma poiché molteplici possono essere i servizi offerti dal sistema e molte le connessioni contemporanee, è necessario un metodo per separare i singoli flussi di dati ed indirizzarli verso il corretto programma di gestione.

A chi non è capitato per esempio di navigare e contemporaneamente di scaricare la posta elettronica o usare un programma p2p? Come fa il sistema a riconoscere a chi sono diretti i dati in arrivo?

Il problema viene risolto attraverso il meccanismo delle porte logiche, nel confronto con l’esempio precedente, fanno le veci del nome del nome del mittente e del destinatario.

Flussi di dati distinti diretti verso la stessa macchina sono caratterizzati da porte diverse.

Una connessione tra due dispositivi, a livello di trasporto, viene univocamente identificata dalle coppie:

  1. “indirizzo IP : porta” del mittente
  2. “indirizzo IP : porta” del destinatario

Ognuna delle coppie “indirizzo IP: porta” rappresenta i dati identificativi di un socket, un oggetto software creato dal livello di trasporto per identificare un dispositivo di rete.

Per identificare una connessione abbiamo dunque bisogno di 2 socket (per individuare il punto di partenza e il punto di arrivo) allo stesso modo in cui tramite due punti nel piano identifichiamo un segmento.

I protocolli del livello di trasporto utilizzano un identificatore a 16 bit (numeri da 0 a 65535), quindi questo rende possibili (teoricamente) fino a 65536 socket contemporaneamente attivi per ogni porta fisica di ogni dispositivo.

In linea di principio i numeri di porta per identificare un socket potrebbero essere scelti arbitrariamente durante la connessione ma, come vedremo, la cosa si rivelerebbe decisamente poco pratica.

Ogni comunicazione sulla rete presume che inizialmente, in una dei due dispositivi coinvolti, ci sia un software “in ascolto”, in attesa di un contatto dall’esterno: un server.

Ad un certo punto un software in esecuzione sul secondo dispositivo tenta di connettersi al primo: il client.

Se un dispositivo collegato ad un rete desidera comunicare con un altro sistema deve necessariamente sapere l’indirizzo completo della macchina remota. In questo caso quindi il client deve essere a conoscenza sia dell’IP sia della porta su cui il server si aspetta di ricevere i dati relativi al servizio di interesse.

Se il server scegliesse la porta in modo casuale nessuno saprebbe come raggiungerlo.

Per questo motivo nasce l’esigenza di standardizzare e rendere pubblici i numeri di porta relativi ai più comuni servizi disponibili (server) su internet e sulle reti in genere.

In questo modo un client che volesse accedere ad un qualsiasi server HTTP saprebbe a priori di doversi collegare alla porta TCP numero 80. Un server di posta SMTP sarà sempre raggiungibile sulla sua porta TCP 25 così come un server FTP userà la TCP 21 e un DNS la porta UDP 53.

Contrariamente al caso precedente il client non ha mai l’esigenza di rendere pubblico il suo indirizzo completo prima della connessione perché questo verrà automaticamente comunicato al server nel momento in cui verrà contattato.

In questo caso quindi il numero di porta del client può essere scelto arbitrariamente dal sistema tra quelli al momento disponibili.

L’Internet Assigned Numbers Authority (IANA) è l’ente che ha tra i suoi scopi la standardizzazione delle porte e l’aggiornamento costante di un documento, chiamato ports-number, contenente l’elenco dei servizi registrati e delle relative porte utilizzate. A questo scopo, nel suddetto documento, lo spazio delle 65536 porte UDP e TCP è stato suddiviso in tre parti:

  1. Porte Note (da 0 a 1023)

    L’accesso a queste porte è riservato a servizi con privilegi amministrativi.
    Tipicamente ogni protocollo del livello superiore ha la sua porta più conosciuta (quella del server) in questo gruppo.

  2. Porte Registrate (da 1024 a 49151)

    L’accesso a queste porte è libero per l’utilizzo da parte di chiunque.
    Di solito, ogni protocollo del livello superiore ha una porta per il server in questo gruppo, in modo che chiunque possa eseguire un server senza privilegi amministrativi, ad esempio per motivi di test, di sviluppo, etc.

    Vi sono inoltre registrati tutti quei servizi che non necessitano di privilegi amministrativi, come i servizi p2p o i servizi di condivisione utente per file e stampanti.

    Ultima tipologia di porte registrate in questa area sono quei client di cui, per necessità del protocollo, è necessario conoscere a priori la porta utilizzata.

  3. Porte Dinamiche (da 49152 a 65535)

    L’accesso a queste porte è libero per l’utilizzo da parte di chiunque e nessuna di esse può essere registrata per uno scopo particolare.
    In questo gruppo tipicamente i client scelgono la loro porta da utilizzare per il socket necessario alla connessione. Nell’improbabile caso della saturazione di questo gruppo sono comunque utilizzabili anche le porte del secondo gruppo rimaste libere.

L’elenco completo delle porte assegnate può essere reperito nel documento pubblicato da IANA al seguente indirizzo web (http://www.iana.org/assignments/port-numbers).

Nella tabella seguente elenco le porte utilizzate dai protocolli più comuni.

Ovviamente dovrete impararle tutte!

Transport Layer Common Ports

Il protocollo TCP

Il protocollo TCP è senza dubbio il principale protocollo che implementa il livello di trasporto nella suite Internet. È stato definito per la prima volta nel 1981 tramite il documento RFC 793, che ha subito successivamente numerosi aggiornamenti.

Esso fornisce un metodo per garantire una consegna sicura e ordinata delle informazioni, utilizzando metodi connessi e affidabili e stabilendo connessioni punto a punto fra mittente e destinatario. Questo implica dunque anche l’impossibilità di supportare broadcasting e multicasting (tecniche di trasmissione “multipla” a livello di rete).

TCP riceve un flusso di dati da una applicazione e lo divide in TPDU grandi solitamente 1500 byte, ma comunque non più di 64 Kb e li passa poi al livello di rete, di cui utilizza sempre il protocollo IP.

L’intestazione del pacchetto TCP è di 20 byte, organizzati nel modo seguente:

TCP Header

Dove specifichiamo i campi più importanti:

  • la porta mittente e destinatario sono le porte logiche TCP del mittente e del destinatario
  • Gli 8 flag (bit) di stato servono per indicare informazioni come i pacchetti SYN, ACK, FIN.
  • Il CRC (cyclic redundancy check) serve a verificare se il pacchetto è corrotto oppure no.

Three way handshake

Il protocollo TCP, per stabilire una connessione utilizza il metodo “three way handshake”, ideato per assicurare al mittente la presenza e la raggiungibilità del destinatario prima di iniziare l’invio vero e proprio dei dati.

3 way handshake

Passo 1

Il client invia una richiesta di connessione in un pacchetto denominato SYN, contenente un identificativo A.

Passo 2

Se il server riceve questa notifica, questi risponde con un pacchetto denominato SYN+ACK.

Il SYN è il pacchetto di sincronizzazione, contenente un identificativo B; l’ACK è la conferma (acknoledgement) del SYN sopraggiunto, contenente l’identificativo A + 1.

Passo 3

Se il client riceve il pacchetto SYN+ACK dal server, per lui la connessione è aperta. Invia inoltre un pacchetto ACK di conferma contenente l’identificativo B + 1.

Se il server riceve questa ulteriore informazione apre anch’esso la connessione con il client.


La chiusura di una connessione avviene in genere anch’essa con il metodo “three way handshake”. Per concordare una chiusura:

una stazione manda un segnale di chiusura e fa partire un timer (passo 1);

la seconda quando riceve il segnale invia una conferma e chiude la connessione (passo 2).

Se la conferma arriva a destinazione, la prima stazione chiude la connessione, altrimenti lo fa comunque allo scadere del tempo cronometrato dal timer.

Questa differenza di comportamento fra apertura e chiusura si esplicita nell’uso dei pacchetti: SYN e ACK per l’apertura, FYN e ACK per la chiusura.

Analogamente, quando la connessione è stabilita e dopo ogni pacchetto inviato, le stazioni fanno partire un timer oltre il quale chiudere la connessione se non si riceve o invia nulla nel frattempo. Questa strategia viene messa in atto per evitare di tenere in piedi connessioni “eterne” fra stazioni che non trasmettono mai.

Per mantenere in piedi le connessioni quindi, il livello deve fare “qualcosa”, altrimenti il timer descritto chiuderà automaticamente la connessione. Se i livelli superiori richiedono una connessione attiva che dura nel tempo anche a fronte di nessun invio di dati (esempio: chat in cui nessuno parla) il livello di trasporto mantiene attiva la connessione inviando ad intervalli regolari dei pacchetti vuoti (dummy), che appunto hanno l’unico scopo di mantenere viva la connessione.

Il protocollo UDP

Il protocollo UDP è uno dei primi protocolli della suite Internet progettati. Il documento RFC 768 che lo definisce è stato reso definitivo nel 1980 e, incredibile a dirsi, non è stato mai più ritoccato o rivisto. Pensate inoltre che è un documento di sole 2 pagine… come dire… presto e bene!

UDP fornisce un metodo per spedire dati senza dover stabilire una connessione con il destinatario (non connesso) e senza dover verificare i dati arrivati ad esso (non affidabile). A dirla tutta non si preoccupa nemmeno di riordinare i pacchetti giunti a destinazione.

Risulta quindi un protocollo molto veloce (leggi: più veloce di TCP, ma molto più limitato) ed è inoltre l’unico protocollo di trasporto abile alle trasmissioni in broadcast e multicast.

L’intestazione dei datagrammi UDP ha il seguente formato:

UDP Header

UDP fornisce soltanto i servizi essenziali del livello di trasporto:

  • multiplazione delle connessioni, tramite il meccanismo di assegnazione delle porte;
  • verifica dell’integrità dei dati, tramite il meccanismo CRC

Tutto questo per ridurre al minimo la latenza della connessione e fornire un servizio rapido e leggero ai protocolli superiori.

Esempi di protocolli che utilizzano UDP sono:

  • DNS (nella versione originale, senza sicurezza)
  • DHCP (forzatamente, poiché invia i pacchetti in broadcast)
  • Tutti i protocolli di streaming audio/video e VoIP (di solito trasmessi su 2 canali: uno TCP per i comandi, uno UDP per il flusso dati)


Python Net Programming

La programmazione di rete, in qualunque linguaggio la si affronti, è una parte della programmazione abbastanza diversa da quella tradizionale.

In questo ambito, non basta studiare la liberia, la sintassi e la logica di sviluppo per fare cose, ma occorre conoscere PRIMA tutta la teoria delle reti relativa ai protocolli con cui si interagisce tramite programmazione.

Python, come al solito, rende l’approccio alle proprie librerie e alla sua logica semplice e veloce: diverso il discorso sulle conoscenze teoriche. Lì non ci sono sconti! O conoscete la teoria e quindi avete le armi per capire ciò che andremo a fare, oppure non riusciremo ad uscire dalla banalità degli esempi proposti.

Socket Programming

I socket sono oggetti software gestiti dal Sistema Operativo. Sono gli unici responsabili per qualunque connessione di rete. Anzi… più precisamente una connessione di rete non è altro che uno scambio di dati fra due socket! E chi chiede al sistema operativo di creare i socket? Le applicazioni!!!

I socket permettono anche ai dispositivi di veicolare contemporaneamente più connessioni logiche (ad esempio, due schede di un browser aperte) attraverso un’unica connessione fisica! Più precisamente, per ogni dispositivo fisico di connessione alla rete sono disponibili 65.536 porte logiche per la possibile creazione di altrettanti socket!

Socket Connection

Come si evince chiaramente dalla figura, Ogni socket si individua grazie alla coppia di informazioni: IP, PORTA LOGICA.

Ogni connessione alla rete viene individuata univocamente (nell’unità di tempo) dalla coppia di socket che fanno da mittente e destinatario della stessa.

Il livello di trasporto può fornire due tipi di servizi, definiti in due protocolli diversi:

  • Il protocollo TCP, per le connessioni punto a punto (1 a 1); connesso e affidabile
  • Il protocollo UDP per le connessioni semplici (anche broadcast e multicast) senza alcuna sovrastruttura: non connesso e non affidabile.

Fino a qui dovreste già sapere tutto! Passiamo al codice, dunque…

Socket in Python

Come dicevamo, i socket sono oggetti software gestiti dal Sistema Operativo e invocati dalle applicazoini. Come si crea un socket in Python??

import socket

# Oggetto Socket TCP
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Oggetto Socket UDP
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

Anche qui… non mi sembra complicato ☺️

Faremo adesso una prova di comunicazione client-server tramite il protocollo UDP.

Comunicazione client/server UDP in Python

Vediamo il codice che crea un server con il protocollo UDP. Questo semplice programma si mette in attesa di comunicazioni. Poi a seconda del messaggio arrivato risponde OK se il numero di lettere arrivate è pari, ERR se sono dispari.

UDP Server in Python
import socket

localIP = "192.168.110.200"  # Qui ci va il tuo IP, come stringa
localPort = 20000            # Qui ci va una porta (> 1024), come intero

# UDP Socket Object
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Bind to address and ip
udp_socket.bind( (localIP, localPort) )

print("UDP server up and listening")

# Listen for incoming datagrams
while True:
    message,address = udp_socket.recvfrom(1024)

    print("Message from Client:", message.decode() )
    print("Client IP Address:", address)

    msgFromServer = "OK"
    if len(message) % 2 == 1:
        msgFromServer = "Err"
    bytesToSend = str.encode(msgFromServer)
    
    # Sending a reply to client
    udp_socket.sendto(bytesToSend, address)

Ovviamente abbinato al server, che deve essere in esecuzione, ci va un client che deve tentare la connessione e l’invio allo stesso. Ecco il codice che permette di inviare al server un messaggio qualunque.

UDP Client in Python
import socket

Server_IP   = "192.168.110.200"  # Qui ci va l'IP del dispositivo che esegue il tuo server, come stringa
Server_PORT = 20000              # Qui ci va la porta del tuo server (la devi sapere), come intero

MESSAGE = input("text to send: ")

print("UDP target IP: ", Server_IP)
print("UDP target port: ", Server_PORT)
print("message: ", MESSAGE)

# UDP Socket Object
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.sendto(MESSAGE.encode(), (Server_IP, Server_PORT))

message,address = udp_socket.recvfrom(1024)

print("Reply from Server:", message.decode() )
print("Server IP Address:", address)

Adesso basta solo provare :)

Esercizi su socket UDP

Alcuni esercizi per prendere confidenza con i socket UDP


Esercizio sock01

Implementare un peer ovvero un programma che contiene contemporaneamente client e server UDP. Eseguirne due copie in grado di colloquiare alternativamente fra loro: il primo manda un msg di testo al secondo, il secondo lo vede e manda una risposta (digitata dall’utente) al primo, il primo legge la risposta e può rispondere… e così all’infinito (finché non si interrompe il programma).


Esercizio sock01 bis

Implementare un peer ovvero un programma che contiene contemporaneamente client e server UDP. Eseguirne tre o quattro copie (opportunamente configurate) in grado di colloquiare fra loro secondo un ordine prestabilito: il primo manda un msg di testo agli altri tre, poi il secondo, poi il terzo, poi il quarto e si ricomincia. Tutti ricevono i messaggi di tutti


Esercizio sock01 ter

Implementare un server in grado di servire quattro persone che parlano fra di loro. Questo riceve un msg da uno qualsiasi dei client e lo invia agli altri tre. Implementare un client in grado di inviare messaggi al server e di riceverli. Modificare il client in modo da stabilire l’ordine con cui le persone parlano (vedi 3301 bis).


Esercizio sock01 quater

Implementare un server in grado di servire un numero imprecisato di client. Ogni volta che un client invia un messaggio questi viene aggiunto alla lista dei client a cui inviare i messaggi degli altri. Il client dell’esercizio precedente dovrebbe funzionare bene anche con questo server.


Esercizio sock01 quinquies GUI

Il server è quello dell’esercizio precedente. Il client ha le stesse funzionalità di prima, ma implementa una interfaccia grafica in wxPython.


Esercizio sock02

Implementare un peer ovvero un programma che contiene contemporaneamente client e server UDP. Entrambi possono scrivere quando vogliono e visualizzano i messaggi che arrivano nell’istante in cui lo fanno.


Esercizio sock02 GUI

Come l’esercizio precedente, ma fornendo una GUI scritta in wxPython


Esercizio sock03

Coppia di programmi che permettono di inviare file di testo semplice fra programma e programma. Individuare il file (ad esempio con l’ausilio della libreria pathlib) e inviate la stringa [NOME_FILE + “\n\n” + CONTENUTO_FILE ]. Il client è in grado di salvare nella stessa cartella dello script il file con il nome inviato.


Esercizio sock03 GUI

Come l’esercizio precedente, ma fornendo una GUI scritta in wxPython. Eventualmente nel programma di ricezione si può permettere all’utente di selezionare DOVE salvare il file

Comunicazione client/server TCP in Python

TCP è un protocollo ben più complicato di UDP! Vi espongo le differenze fondamentali:

  • il socket del server, prima di poter ricevere connessioni, dovrà porsi in modalità listen
  • il client, prima di poter inviare dati al server, dovrà stabilire una connessione con il socket del server
  • Una volta stabilita la connessione, i dati tra i due socket saranno veicolati tramite essa, con i metodi sendall e recv
  • I dati di passaggio nella connessione sono per forza di cose ordinati! (A questo ci pensa TCP… noi non dobbiamo fare nulla. Solo sapere…)
  • Alla fine dello scambio di dati la connessione va chiusa con il metodo close da parte di entrambi i socket!!!

Vediamo un esempio di codice che implementa un semplice client e un semplice server basati su TCP. Il client potrà inviare qualsiasi messaggio vuole, mentre il server risponderà comunque ok. Se il client invia la stringa close il server saluta con bye e chiude la connessione.

Simple Python TCP server
import socket

HOST = "127.0.0.1"  # Standard loopback interface address (localhost)
PORT = 65432        # Port to listen on (non-privileged ports are > 1023)

tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

tcp_socket.bind( (HOST, PORT) )
tcp_socket.listen()

print(f"listening on socket ({HOST}:{PORT})...")

conn, addr = tcp_socket.accept()

if conn:
    print(f"Connected by {addr}")
    
    while True:
        data = conn.recv(1024)
        message = data.decode()
        print("Received:", message)
        if message == "close":
            conn.sendall( "bye".encode() )
            break
        conn.sendall( "OK".encode() )
        
conn.close()

Qui sotto invece trovate il codice che implementa il client TCP.

Simple Python TCP client
import socket

HOST = "127.0.0.1"  # The server's hostname or IP address
PORT = 65432  # The port used by the server

tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_socket.connect( (HOST, PORT) )

while True:
    message = input("Message to send: ")

    tcp_socket.sendall( message.encode() )
    data = tcp_socket.recv(1024)
    message = data.decode()
    print(f"Received: {data.decode()}")

    if message == "bye":
        tcp_socket.close()
        break

Esercizi su socket TCP


Esercizio sock11

Implementare un peer ovvero un programma che contiene contemporaneamente client e server TCP. Eseguirne due copie in grado di colloquiare alternativamente fra loro: il primo manda un msg di testo al secondo, il secondo lo vede e manda una risposta (digitata dall’utente) al primo, il primo legge la risposta e può rispondere… e così all’infinito (finché non si interrompe il programma).


Esercizio sock11 bis

Implementare un peer ovvero un programma che contiene contemporaneamente client e server TCP. Eseguirne tre o quattro copie (opportunamente configurate) in grado di colloquiare fra loro secondo un ordine prestabilito: il primo manda un msg di testo agli altri tre, poi il secondo, poi il terzo, poi il quarto e si ricomincia. Tutti ricevono i messaggi di tutti


Esercizio sock11 ter

Implementare un server in grado di servire quattro persone che parlano fra di loro. Questo riceve un msg da uno qualsiasi dei client e lo invia agli altri tre. Implementare un client in grado di inviare messaggi al server e di riceverli. Modificare il client in modo da stabilire l’ordine con cui le persone parlano (vedi 3401 bis).


Esercizio sock11 quater

Implementare un server in grado di servire un numero imprecisato di client. Ogni volta che un client invia un messaggio questi viene aggiunto alla lista dei client a cui inviare i messaggi degli altri. Il client dell’esercizio precedente dovrebbe funzionare bene anche con questo server.


Esercizio sock11 quinquies GUI

Il server è quello dell’esercizio precedente. Il client ha le stesse funzionalità di prima, ma implementa una interfaccia grafica in wxPython.


Esercizio sock12

Implementare un peer ovvero un programma che contiene contemporaneamente client e server TCP. Entrambi possono scrivere quando vogliono e visualizzano i messaggi che arrivano nell’istante in cui lo fanno.


Esercizio sock12 GUI

Come l’esercizio precedente, ma fornendo una GUI scritta in wxPython


Esercizio sock13

Coppia di programmi che permettono di inviare file di testo semplice fra programma e programma. Individuare il file (ad esempio con l’ausilio della libreria pathlib) e inviate la stringa [NOME_FILE + “\n\n” + CONTENUTO_FILE ]. Il client è in grado di salvare nella stessa cartella dello script il file con il nome inviato.


Esercizio sock13 GUI

Come l’esercizio precedente, ma fornendo una GUI scritta in wxPython. Eventualmente nel programma di ricezione si può permettere all’utente di selezionare DOVE salvare il file

Python requests

Il venerabile modulo requests è lo standard de facto in Python per realizzare, gestire, analizzare richieste e risposte HTTP. Questa libreria non fa parte della dotazione ufficiale standard e va quindi installata, tramite l’interfaccia di Thonny o tramite il comando:

pip install requests

Warning

Il protocollo HTTP!!!

Se siete qui a studiare il modulo requests, si presuppone abbiate già una discreta conoscenza del protocollo HTTP.

La sintassi di base del modulo è strutturata nel modo seguente:

import requests

# method è una funzione generica per uno dei metodi HTTP: get, post, put, head ... etc...
response = requests.method( URL , options )

# options è un contenitore generico per varie opzioni:
# - data=json_data per i dati da inviare in POST
# - headers=dict_headers per il dizionario degli headers da aggiungere alla richiesta
#
# response.status_code contiene lo status code della risposta
# response.text contiene il corpo della risposta come stringa
# response.content contiene il corpo della risposta come bytes

Richieste GET

Il metodo GET è quello che usiamo quando vogliamo ottenere dati da un server, senza modificarne lo stato.

import requests

url = "https://api.open-meteo.com/v1/forecast"
params = {
    "latitude": 43.6,
    "longitude": 13.5,
    "current": "temperature_2m,relative_humidity_2m,wind_speed_10m"
}

response = requests.get(url, params=params)

print(response.status_code)   # 200 se la richiesta è andata a buon fine
print(response.url)           # l'URL completo, costruito automaticamente da params

Note

Il dizionario passato con params= viene automaticamente trasformato da requests in una query string. Provate a stampare response.url: vedrete comparire qualcosa come https://api.open-meteo.com/v1/forecast?latitude=43.6&longitude=13.5&current=temperature_2m...

Costruire questa stringa a mano, concatenando & e =, è esattamente il tipo di lavoro noioso e pieno di errori da cui una libreria come requests ci libera.

Il corpo della risposta si può leggere in diversi modi:

response.text     # corpo come stringa grezza
response.content  # corpo come bytes
response.json()   # corpo interpretato come JSON e convertito in dict/list Python

Sostanzialmente andremo ad usare respose.text nei nostri script per visualizzare i risultati come stringhe; useremo response.json() quando la risposta è un JSON generato a partire da una API (per l’interazione di due app senza reale intervento umano) e response.content quando la risposta è in formato binario (immagini, file, ecc.).

dati = response.json()
print(dati["current"]["temperature_2m"])

Warning

.json() solleva un’eccezione se il corpo della risposta non è JSON valido (ad esempio se il server ha risposto con una pagina di errore HTML). Vedremo tra poco come gestire questi casi.

Un esempio pratico: temperature a confronto

Mettiamo insieme quanto visto per scrivere qualcosa di utile: un piccolo script che confronta la temperatura attuale di alcune città.

import requests

url = "https://api.open-meteo.com/v1/forecast"
città = {
    "Ancona": (43.6, 13.5),
    "Milano": (45.46, 9.19),
    "Roma": (41.9, 12.5)
}

for nome, (lat, lon) in città.items():
    params = {"latitude": lat, "longitude": lon, "current": "temperature_2m"}
    dati = requests.get(url, params=params).json()
    temperatura = dati["current"]["temperature_2m"]
    print(f"{nome}: {temperatura}°C")

Notate che il servizio Open-Meteo usato in questo esempio non richiede alcuna registrazione né API key: è comodo proprio per questo, oltre al fatto di restituire dati reali e verificabili.

Richieste POST

Il metodo POST si usa quando vogliamo inviare dati al server, tipicamente per creare qualcosa (un nuovo utente, un nuovo post, un ordine…). requests offre due modalità per inviare dati con una POST:

import requests

# data=: invia i dati come form-encoded, esattamente come farebbe
# un <form> HTML inviato con method="post"
r1 = requests.post("https://httpbin.org/post", data={"nome": "Andrea", "corso": "Reti"})

# json=: serializza automaticamente il dizionario in JSON
# e imposta da solo l'header Content-Type: application/json
r2 = requests.post("https://httpbin.org/post", json={"nome": "Andrea", "corso": "Reti"})

Tip

httpbin.org è un servizio pensato apposta per fare pratica con le richieste HTTP: qualsiasi cosa gli mandiate, ve la restituisce indietro dentro la risposta, così potete vedere esattamente cosa è arrivato al server.

print(r1.json()["form"])   # {'nome': 'Andrea', 'corso': 'Reti'}
print(r2.json()["json"])   # {'nome': 'Andrea', 'corso': 'Reti'}

Note

Notate le due chiavi diverse nella risposta, "form" e "json": httpbin.org distingue i dati arrivati come form-encoded da quelli arrivati come JSON. Nella pratica, oggi la maggior parte delle API REST si aspetta json=.

Gestione degli errori

Una richiesta di rete può fallire in tanti modi: il server non risponde, ci mette troppo tempo, restituisce un errore. requests non solleva eccezioni per gli status code di errore (tipo 404 o 500) a meno che non lo chiediate esplicitamente con raise_for_status().

import requests

try:
    response = requests.get("https://httpbin.org/delay/5", timeout=2)
    response.raise_for_status()
except requests.exceptions.Timeout:
    print("Il server ci ha messo troppo tempo a rispondere")
except requests.exceptions.HTTPError as e:
    print(f"Errore HTTP: {e}")
except requests.exceptions.RequestException as e:
    print(f"Qualcosa è andato storto: {e}")

Warning

Senza timeout=, uno script che interroga un server irraggiungibile può rimanere bloccato indefinitamente. Impostare sempre un timeout è buona pratica, non un dettaglio opzionale.

Tip

https://httpbin.org/delay/5 è un endpoint che risponde apposta dopo 5 secondi: con timeout=2 l’eccezione Timeout scatterà sempre, permettendovi di vederla “dal vivo” senza dover simulare un guasto di rete vero.

Esercizi

Esercizio req01

Scrivete uno script che interroghi l’API di Open-Meteo per la vostra città e stampi temperatura, umidità e velocità del vento attuali, in un formato leggibile tipo:

Meteo a Senigallia:
  Temperatura: 28.4°C
  Umidità: 61%
  Vento: 12.3 km/h

Tip

Potete trovare latitudine e longitudine della vostra città con una rapida ricerca, oppure usare il servizio di geocoding di Open-Meteo (https://geocoding-api.open-meteo.com/v1/search?name=NOMECITTÀ).


Esercizio req02

Modificate lo script dell’esercizio precedente in modo che la città venga chiesta all’utente con input(), invece di essere scritta nel codice. Gestite il caso in cui la città non venga trovata dal servizio di geocoding (la risposta JSON in tal caso non conterrà risultati: come ve ne accorgete?).


Esercizio req03

Scrivete uno script che invii una richiesta POST a https://httpbin.org/post con un dizionario contenente il vostro nome, cognome e classe, usando json=. Stampate poi a schermo l’intera risposta con response.json() e individuate, dentro il JSON restituito, il punto esatto in cui ritrovate i dati che avete inviato.


Esercizio req04

Scrivete uno script che tenti una richiesta GET verso un indirizzo che non esiste (es. https://questositosicuramentenonesiste12345.com), gestendo correttamente l’eccezione con un messaggio d’errore chiaro per l’utente, invece di far crashare il programma. Ripetete poi l’esercizio usando https://httpbin.org/delay/10 con un timeout breve, per osservare la differenza tra un errore di connessione e un timeout.


Esercizio req05

Scrivete un piccolo programma “stazione meteo da terminale” che:

  1. chiede all’utente il nome di una città;
  2. usa il geocoding di Open-Meteo per ottenere latitudine e longitudine;
  3. usa quei dati per interrogare le previsioni per i prossimi 3 giorni (suggerimento: parametro daily=temperature_2m_max,temperature_2m_min);
  4. stampa un piccolo riepilogo giorno per giorno;
  5. gestisce in modo appropriato ogni possibile errore (città non trovata, timeout, errore di connessione), senza mai far crashare il programma.

I Linguaggi del Web

La rete Internet è un collegamento esteso a livello mondiale, che rende fruibile diversi servizi utilizzabili dagli utenti. Fra questi, uno dei servizi più noti è il cosiddetto World Wide Web, il WWW o semplicemente WEB per gli amici 😉

Il WEB è il servizio che permette di visitare una serie di pagine (WEB, appunto) definite in linguaggio tecnico “ipertesti”.

HTTP è il protocollo di trasferimenti degli ipertesti dai server web che ospitano i siti ai browser nei computer delle persone che vogliono navigare (nel WEB). Le pagine web sono scritte facendo “collaborare” fra loro un mix di tecnologie, tra cui:

  • HTML, il linguaggio di markup delle pagine web
  • CSS, il linguaggio che serve a definire gli stili delle pagine
  • JAVASCRIPT, un linguaggio di programmazione web lato client

Questi primi esempi non esauriscono la quantità di tecnologia diversa e “organizzata” tramite la quale è composto il www, ma di sicuro rappresentano i linguaggi più importanti su cui questo si basa.

Anatomia di una pagina web

Prima di scrivere una riga di HTML, vediamo cosa succede quando digiti un indirizzo nel browser: il browser manda una richiesta al server, il server risponde con un file di testo scritto in HTML, e il browser lo interpreta per disegnare la pagina che vedi.

HTML (HyperText Markup Language) non è un linguaggio di programmazione: non ci sono cicli, condizioni o funzioni. È un linguaggio di markup: descrive la struttura di un documento usando dei tag.

La struttura minima

Ogni pagina HTML parte da questo scheletro:

index.html
<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <title>La mia prima pagina</title>
</head>
<body>
    <h1>Ciao, mondo!</h1>
</body>
</html>

Vediamo cosa fa ogni riga:

  • <!DOCTYPE html> dice al browser “questo è un documento HTML5”. Va sempre messo per primo.
  • <html lang="it"> è il tag radice, contiene tutta la pagina. lang="it" dice al browser (e ai motori di ricerca) che la lingua è l’italiano.
  • <head> contiene informazioni sulla pagina, che non vengono mostrate direttamente: titolo della scheda del browser, collegamenti a fogli di stile, metadati.
  • <body> contiene tutto quello che l’utente vede.

Note

In Python l’indentazione è obbligatoria e definisce i blocchi.
In HTML l’indentazione è solo una convenzione per rendere il codice leggibile:
è la coppia di tag di apertura/chiusura (<body></body>) a definire dove inizia e finisce un blocco.

Tag di apertura, chiusura e “vuoti”

La maggior parte dei tag va aperta e chiusa:

<p>Questo è un paragrafo.</p>

Alcuni tag invece non contengono nulla e non hanno bisogno di chiusura — si dicono tag vuoti (void elements):

<br>
<hr>
<img src="foto.jpg" alt="Descrizione della foto">

Attributi

I tag possono avere attributi, scritti dentro il tag di apertura come coppie nome="valore":

<a href="https://www.adjam.org" title="Il sito del prof">Vai al sito</a>

Qui href dice dove porta il link, title è il testo che appare al passaggio del mouse.

Warning

Gli attributi vanno sempre tra virgolette. <img src=foto.jpg> spesso “funziona” per tolleranza del browser, ma non è HTML valido — evitatelo.

Tag semantici vs generici

HTML mette a disposizione due tag “neutri”, senza significato proprio:

  • <div> — un contenitore generico di tipo block (va a capo prima e dopo)
  • <span> — un contenitore generico di tipo inline (non va a capo)

Ma dove possibile è meglio usare tag semantici, che comunicano il significato del contenuto sia al browser sia a chi legge il codice: <h1><h6> per i titoli, <p> per i paragrafi, <strong> per il testo importante, <em> per l’enfasi. Nei prossimi capitoli ne vedremo altri (<nav>, <header>, <footer>…), adatti alla struttura della pagina.

Commenti

<!-- Questo è un commento, il browser lo ignora -->

Esercizi

Esercizio w101

Scrivi lo scheletro minimo di una pagina HTML con titolo della scheda "Chi sono" e un unico <h1> con il tuo nome.


Esercizio w102

Nel seguente codice ci sono 3 errori (tag non chiusi, attributi senza virgolette, DOCTYPE scritto male). Trovali e correggili:

da_correggere.html
<!doctype htlm>
<html lang=it>
<head>
    <title>Pagina con errori
</head>
<body>
    <h1>Benvenuto
    <p>Questo paragrafo non si chiude.</body>
</html>

Esercizio w103

Aggiungi un commento HTML sopra il tag <body> che spieghi a cosa serve la pagina.

Tag di base

Ho definito tag di base quelli che servono per le banali operazioni di inserimento di Testo, liste, link e immagini. Mi sembrano proprio alla base di tutto il discorso…

Paragrafi e titoli

Il testo di una pagina si organizza in paragrafi (<p>) e titoli su 6 livelli di importanza (<h1> il più importante, <h6> il meno importante):

testo.html
<h1>Il mio blog</h1>
<h2>Ultimo articolo</h2>
<p>Oggi ho scoperto quanto è comodo scrivere pagine web.</p>
<p>Questo è un secondo paragrafo, separato dal primo.</p>

Ogni pagina dovrebbe avere un solo <h1> (di solito il titolo principale), poi <h2> per le sezioni, <h3> per le sottosezioni, e così via — è la stessa logica gerarchica di un indice di un libro.

Formattazione del testo

<p>Questo testo è <strong>molto importante</strong>, questo invece è <em>enfatizzato</em>.</p>
<p>Puoi anche andare a capo<br>senza iniziare un nuovo paragrafo.</p>

<strong> (importante, grassetto) e <em> (enfatizzato, corsivo) comunicano non sono solo una modalità grafica di resa del testo, ma anche una evidenziazione semantica: uno screen reader, ad esempio, li legge con un tono diverso.

Tip

Per il solo effetto grafico, senza significato, esistono <b> (bold, grassetto) e <i> (italic, corsivo), ma ormai sono sempre da preferire le loro versioni semantiche <strong> e <em>.

Liste

Esistono due tipi di lista:

liste.html
<h2>Lista puntata</h2>
<ul>
    <li>HTML</li>
    <li>CSS</li>
    <li>JavaScript</li>
</ul>

<h2>Lista numerata</h2>
<ol>
    <li>Scrivi l'HTML</li>
    <li>Aggiungi lo stile con CSS</li>
    <li>Aggiungi il comportamento con JS</li>
</ol>

<ul> (unordered list) per elenchi senza ordine, <ol> (ordered list) quando l’ordine conta (es. istruzioni passo-passo). In entrambi i casi ogni voce è un <li> (list item).

Le liste si possono anche annidare:

<ul>
    <li>Frontend
        <ul>
            <li>HTML</li>
            <li>CSS</li>
        </ul>
    </li>
    <li>Backend</li>
</ul>

Un link si crea col tag <a> (anchor) e l’attributo href:

<a href="https://www.adjam.org">Vai al sito del prof!</a>
<a href="pagina2.html">Vai alla pagina 2</a>
<a href="#sezione3">Vai alla sezione 3 di questa pagina</a>

Tre casi diversi:

  • un URL completo (link esterno, a un altro sito)
  • un percorso relativo tipo pagina2.html (link interno, a un altro file dello stesso sito)
  • #sezione3 (link a un ancora dentro la stessa pagina — richiede un elemento con id="sezione3" da qualche parte)

Immagini

<img src="logo.png" alt="Logo della scuola" width="200">
  • src è il percorso del file immagine (relativo o assoluto, come per i link)
  • alt è il testo alternativo, mostrato se l’immagine non si carica e letto dagli screen reader — non è opzionale, va sempre messo
  • width (e height) impostano le dimensioni; è meglio farlo con il CSS (lo vedremo), ma è utile saperlo fare anche qui

Note

<img> è un tag vuoto, come <br>: non si chiude con </img>.

Esercizi

Esercizio w111

Crea una pagina con un <h1> col titolo “I miei linguaggi preferiti” e una lista puntata con almeno 3 linguaggi di programmazione.


Esercizio w112

Trasforma la lista dell’esercizio precedente in una lista numerata, ordinata dal linguaggio che conosci meglio a quello che conosci meno.


Esercizio w113

Crea una pagina con due paragrafi. Nel primo paragrafo inserisci un link esterno (a un sito a tua scelta) e un link interno a un file pagina2.html (non serve che esista davvero). Nel secondo paragrafo inserisci un’immagine con alt descrittivo (anche se il file non esiste).


Esercizio w114

Nel seguente codice manca l’attributo alt su un’immagine e una lista usa <ol> al posto di <ul> per un elenco che non ha un ordine logico. Correggi entrambi i problemi:

da_correggere.html
<h2>I miei hobby</h2>
<ol>
    <li>Lettura</li>
    <li>Fotografia</li>
    <li>Escursionismo</li>
</ol>
<img src="foto_montagna.jpg">

Form e input

I form sono il modo in cui una pagina raccoglie dati dall’utente e li invia da qualche parte — tipicamente a un server. È il capitolo più importante di questo blocco per un corso di reti, perché qui HTML incontra direttamente HTTP.

Struttura di un form

form_base.html
<form action="/invia" method="get">
    <label for="nome">Nome:</label>
    <input type="text" id="nome" name="nome">

    <button type="submit">Invia</button>
</form>

Tre elementi chiave:

  • <form> racchiude tutti i campi. Ha due attributi fondamentali: action (dove vengono inviati i dati) e method (come vengono inviati — lo vediamo tra poco).
  • <label for="nome"> è l’etichetta del campo. L’attributo for deve coincidere con l’id dell’<input> a cui si riferisce: cliccando sulla label si attiva anche il campo, ed è essenziale per l’accessibilità.
  • <input> è il campo vero e proprio. L’attributo name è quello che conta di più: è la chiave con cui il valore verrà inviato al server (l’id invece serve solo per collegare la label e per CSS/JS).

Tip

Pensa a name come alla chiave di un dizionario Python: il form, quando viene inviato, produce qualcosa concettualmente simile a {"nome": "Andrea"}. Il server riceve quel “dizionario” e ci legge dentro con name.

Tipi di input

<input type="text" name="nome" placeholder="Il tuo nome">
<input type="email" name="email" placeholder="nome@esempio.it">
<input type="password" name="pw">
<input type="number" name="eta" min="0" max="120">
<input type="checkbox" name="iscrizione" checked>
<input type="radio" name="livello" value="base">
<input type="radio" name="livello" value="avanzato">

Il type cambia sia la tastiera/interfaccia che il browser mostra, sia una minima validazione automatica (es. type="email" rifiuta un invio senza @). I radio con lo stesso name formano un gruppo: se ne può scegliere uno solo.

Altri elementi comuni nei form, che non sono <input>:

<textarea name="messaggio" rows="4" cols="30"></textarea>

<select name="corso">
    <option value="python">Python</option>
    <option value="reti">Reti</option>
</select>

GET vs POST: cosa succede davvero

Questo è il punto in cui HTML tocca direttamente ciò che abbiamo studiato in HTTP.

get_esempio.html
<form action="/cerca" method="get">
    <input type="text" name="q">
    <button type="submit">Cerca</button>
</form>

Con method="get", il browser mette i dati nell’URL stesso, come query string: /cerca?q=reti dove la coppia q=reti è ovviamente la coppianame=value estratta dal form: q è il nome della text input, reti è il valore inserito dall’utente.

post_esempio.html
<form action="/registrazione" method="post">
    <input type="text" name="nome">
    <input type="password" name="pw">
    <button type="submit">Registrati</button>
</form>

Con method="post" invece i dati viaggiano nel corpo della richiesta, non nell’URL: non sono visibili nella barra degli indirizzi, non finiscono nella cronologia del browser, non c’è un limite pratico di lunghezza.

Quando usare l’uno o l’altro:

GETPOST
Dati visibili in URLNo
Adatto a dati sensibili (password)No
Si può ripetere/salvare come preferitoNo

Note

L’ultima domanda in particolare spiega bene il diverso utilizzo dei due metodi:

Quando si usa GET, l’URL contiene tutto!! Ogni richiesta dovrebbe generare analogia risposta. Tipicamente GET si usa per fare una ricerca o per impostare dei filtri.

Quando si usa POST, i dati sono nel body della richiesta e dipendono dall’interazione utente con la pagina. Tipicamente POST si usa per inviare dati sensibili o lunghi, quali campi di login, moduli da compilare, etc…

Tip

Usate gli strumenti di sviluppo del browser (scheda Network) per osservare la differenza reale tra una richiesta GET e una POST: con GET vedrete i parametri nell’URL della richiesta, con POST li troverete invece nel corpo.

Campi obbligatori (validazione base del browser)

<input type="text" name="nome" required>
<input type="email" name="email" required>

L’attributo required fa sì che il browser stesso blocchi l’invio se il campo è vuoto, senza bisogno di scrivere codice. È una validazione minima e non sostituisce controlli lato server — e non sostituisce nemmeno la validazione lato client più sofisticata che vedremo con JavaScript più avanti!

Esercizi

Esercizio w121

Crea un form con method="get" che invii una ricerca: un campo di testo con name="q" e un pulsante di invio. Dopo averlo aperto nel browser, prova a inviarlo e osserva cosa appare nell’URL.


Esercizio w122

Crea un form di registrazione con method="post" che contenga: nome (testo), email, password, e un pulsante di invio. Aggiungi required ai campi che secondo te non dovrebbero mai essere lasciati vuoti.


Esercizio w123

Nel seguente form manca il collegamento tra <label> e <input> (l’attributo for non corrisponde a nessun id), e la password viaggia con method="get", il che è sbagliato. Correggi entrambi i problemi:

da_correggere.html
<form action="/login" method="get">
    <label for="username">Nome utente:</label>
    <input type="text" name="user">

    <label for="password">Password:</label>
    <input type="password" name="pw">

    <button type="submit">Accedi</button>
</form>

Esercizio w124

Crea un form con un <select> con almeno 3 opzioni (es. una scelta tra 3 corsi) e un gruppo di 2 <input type="radio"> con lo stesso name per una scelta binaria (es. “principiante”/“esperto”).

Layout semantico e tabelle

I tag semantici per la struttura

Nei capitoli iniziali abbiamo visto <div> come contenitore generico. Ora vediamo i tag che HTML5 offre per dare un nome alle grandi aree di una pagina, invece di usare solo <div> ovunque:

<body>
    <!-- l'intestazione della pagina web: loghi, titolo, menù di navigazione -->
    <header>
        <h1>Il mio sito</h1>
        <!-- il menù di navigazione principale -->
        <nav>
            <a href="index.html">Home</a>
            <a href="chi_sono.html">Chi sono</a>
            <a href="contatti.html">Contatti</a>
        </nav>
    </header>

    <!-- il contenuto principale della pagina: ovviamente ci sarà un solo main per ogni pagina!!!   -->   
    <main>
        <!-- un contenuto autonomo, inserito qui: un articolo, una sezione, un post, una news... -->
        <article>
            <h2>Titolo articolo</h2>
            <p>Contenuto dell'articolo...</p>
        </article>

        <!-- un contenuto correlato, inserito qui: ad esempio, un articolo secondario o un approfondimento -->   
        <aside>
            <h3>Articoli correlati</h3>
            <ul>
                <li><a href="#">Un altro articolo</a></li>
            </ul>
        </aside>
    </main>

    <!-- il piè di pagina: crediti per l'autore, il copyright, link importanti da evidenziare -->
    <footer>
        <p>&copy; 2026 Il mio sito</p>
    </footer>
</body>

Vediamo una tabella riassuntiva dei tag semantici utilizzati:

TagDescrizione
<header>Intestazione della pagina (o di una sezione): loghi, titolo, a volte la navigazione
<nav>Un blocco di link di navigazione principali
<main>Il contenuto principale della pagina (uno solo per pagina)
<article>Un contenuto autonomo, che avrebbe senso anche da solo (un post di blog, una notizia)
<aside>Contenuto correlato ma secondario (una sidebar, un box di approfondimento)
<footer>Piè di pagina: crediti, copyright, link secondari

Nessuno di questi tag cambia l’aspetto grafico di default (per quello serve il CSS) — il vantaggio è semantico: un motore di ricerca o uno screen reader capisce la struttura logica della pagina, non solo un mucchio di <div> indistinguibili.

Tip

Una buona domanda da farsi: “questo blocco di contenuto ha già un nome preciso (intestazione, articolo, barra laterale…)?”

Se sì, usa il tag semantico. Se è davvero solo un contenitore senza ruolo specifico (ad esempio per raggruppare elementi ai fini del CSS), allora <div> va benissimo.

Tabelle

Le tabelle servono per dati tabellari — righe e colonne di informazioni collegate

Warning

In passato (prima degli smartphone) le tabelle venivano usate per organizzare il layout…

Non vi azzardate!!!

Per i layout si usa il CSS!!!

tabella.html
<table>
    <thead>
        <tr>
            <th>Nome</th>
            <th>Voto</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Anna</td>
            <td>8</td>
        </tr>
        <tr>
            <td>Marco</td>
            <td>7</td>
        </tr>
    </tbody>
</table>
  • <table> racchiude tutta la tabella
  • <thead> contiene la riga (o le righe) di intestazione
  • <tbody> contiene le righe di dati veri e propri
  • <tr> (table row) è una riga
  • <th> (table header) è una cella di intestazione (in grassetto e centrata di default)
  • <td> (table data) è una cella di dato normale

Una cella può occupare più colonne o più righe con colspan/rowspan:

<tr>
    <td colspan="2">Questa cella occupa 2 colonne</td>
</tr>

Esercizi

Esercizio w131

Crea la struttura semantica di una pagina con <header> (contenente un <h1> e una <nav> con 3 link), <main> con un <article>, e <footer> con una riga di copyright.


Esercizio w132

Crea una tabella con intestazione (<thead>) che riporti “Materia” e “Ore settimanali”, e almeno 4 righe di dati (<tbody>) con materie scolastiche a scelta.


Esercizio w133

Nel seguente codice, un blocco che dovrebbe essere una sidebar di approfondimento usa <div> invece del tag semantico corretto, e una tabella usa <div> per le righe invece di <tr>/<td>. Correggi entrambi i problemi:

da_correggere.html
<div>
    <h3>Vedi anche</h3>
    <a href="#">Altro articolo</a>
</div>

<table>
    <div>
        <div>Nome</div>
        <div>Anna</div>
    </div>
</table>

Esercizio w134

Prendi la pagina del profilo/scheda prodotto dell’esercizio del Progetto 1 (se non l’hai ancora iniziata, va bene anche una pagina qualsiasi con <header>/<main>/<footer>) e aggiungi una tabella con almeno 2 righe e 2 colonne di dati a tua scelta, coerenti col contenuto della pagina.

Progetto Finale: Step 1

Ora che conosci struttura, testo, liste, link, immagini, form, layout semantico e tabelle, mettili insieme in un’unica pagina.

Consegna

Crea una pagina HTML (solo HTML) che rappresenti la scheda di un prodotto, a tua scelta (un oggetto, un videogioco, un libro, un dispositivo… fai tu).

La pagina deve contenere obbligatoriamente:

  • struttura semantica (<header>, <main>, <footer> almeno)
  • un <h1> col nome del prodotto
  • almeno un’immagine del prodotto con alt descrittivo
  • una lista (puntata o numerata) con almeno 3 caratteristiche/funzionalità del prodotto
  • una tabella con almeno 2 righe di specifiche tecniche (es. “Specifica / Valore”)
  • un form “Richiedi informazioni” o “Contatta il venditore” con almeno 3 campi diversi (testo, email, e uno tra checkbox/radio/select) e method="post"

Vincoli

  • Il codice deve essere HTML valido (niente tag non chiusi, attributi senza virgolette)
  • Usa i tag semantici dove ha senso, non solo <div>
  • Ogni <label> deve essere collegata correttamente al suo <input>

CSS

Con l’HTML abbiamo definito la struttura del contenuto. Da qui in poi ci occupiamo dell’aspetto: colori, spaziature, layout. Il linguaggio è il CSS (Cascading Style Sheets).

Collegare un foglio di stile

index.html
<head>
    <meta charset="UTF-8">
    <title>La mia pagina</title>
    <link rel="stylesheet" href="stile.css">
</head>

<link rel="stylesheet" href="..."> va sempre nel <head>. Da qui in poi lavoriamo in un file separato, stile.css.

Note

Esistono altri modi per includere CSS nelle proprie pagine web:

  • CSS interno: Consiste nel mettere un tag <style> all’interno della sezione <head> e lì dichiarare tutte le regole CSS della pagina.
  • CSS inline: Usa l’attributo style direttamente nei tag HTML (<p style="color: red;">). Si applica solo a quel tag specifico.

Evitali entrambi: mescolano struttura e stile, e diventano ingestibili appena la pagina cresce. Un file .css separato è sempre la scelta giusta.

Sintassi CSS di base

File stile.css
selettore {
    proprietà: valore;
    proprietà: valore;
}

Esempio concreto:

p {
    color: blue;
    font-size: 18px;
}

Questa regola dice: “tutti i paragrafi (<p>) avranno testo di colore blue e dimensione 18 pixel”.

Selettori principali

/* Selettore di tag: colpisce TUTTI gli elementi di quel tipo */
p {
    line-height: 1.5;
}

/* Selettore di classe: colpisce ogni elemento con class="evidenziato" */
.evidenziato {
    background-color: yellow;
}

/* Selettore di id: colpisce UN SOLO elemento, quello con id="titolo-principale" */
#titolo-principale {
    text-align: center;
}

E nell’HTML:

<h1 id="titolo-principale">Benvenuti</h1>
<p class="evidenziato">Questo paragrafo è evidenziato.</p>
<p>Questo no.</p>

Regola pratica: usa le classi per stili che si ripetono su più elementi, gli id solo per un elemento unico nella pagina (un id, per definizione, non dovrebbe comparire due volte nello stesso documento).

Si possono anche combinare selettori:

/* Solo i <p> con classe "evidenziato" */
p.evidenziato {
    font-weight: bold;
}

/* Il figlio diretto <li> dentro <nav> */
nav > li {
    display: inline;
}

/* Qualunque discendente <a> dentro .card, a qualunque livello */
.card a {
    text-decoration: none;
}

Chi vince quando due regole si contraddicono?

Partiamo da un esempio che introduce il problema delle regole a cascata.

p {
    color: blue;
}

.evidenziato {
    color: red;
}
<p class="evidenziato">Di che colore sarà questo testo?</p>

CSS significa letteralmente “Cascading Style Sheets” (fogli di stile a cascata) e questo perché stabilisce un ordine di applicazione per le regole. Questo genera anche un ordine di priorità, nel senso che le regole applicate dopo vinceranno su quelle applicate prima.

L’ordine di applicazione delle regole è il seguente:

  1. Il CSS esterno (ad esempio, il file stile.css)
  2. Il CSS interno (presente nel tag <style>)
  3. Il CSS inline (l’attributo style="...")

All’interno di ognuno di questi livelli, le regole vengono applicate secondo l’ordine di specificità:

  1. Tag (p)
  2. Classe (.evidenziato), attributo, pseudo-classe
  3. Id (#titolo)

A parità di specificità, vince la regola scritta più in basso nel file. Se due regole hanno lo stesso identico selettore, l’ultima sovrascrive la prima.

Tip

Considerando che ci siamo già detti di NON usare CSS interno o il CSS inline, le uniche regole interessanti per noi sono quelle tag-class-id. Non dovrebbe essere difficilissimo ricordarlo.

Quindi… di che colore è il testo sopra??

Il box model

Ogni elemento HTML, quando viene renderizzato, è un rettangolo composto da quattro strati concentrici:

div {
    width: 200px;
    padding: 20px;
    border: 2px solid black;
    margin: 10px;
}

Dall’interno verso l’esterno:

  • content — il contenuto vero e proprio (testo, immagine…), largo width
  • padding — lo spazio interno, tra il contenuto e il bordo
  • border — il bordo vero e proprio
  • margin — lo spazio esterno, tra il bordo dell’elemento e ciò che sta intorno

CSS Box Model

Warning

Un dettaglio che confonde spesso: di default, width: 200px si applica solo al content. Se aggiungi padding e border, la larghezza totale dell’elemento cresce oltre i 200px dichiarati.

Per evitare sorprese, è comune impostare globalmente:

* {
    box-sizing: border-box;
}

Con box-sizing: border-box, width include padding e border: l’elemento resta largo esattamente 200px, e il padding “mangia” spazio dal contenuto invece di aggiungersi.

Esercizi

Esercizio w201

Crea un file stile.css che imposti: colore di sfondo della pagina (body) grigio chiaro, tutti i paragrafi con font-size: 16px, e tutti i titoli <h1> centrati.


Esercizio w202

Data questa pagina HTML, scrivi un CSS che coloni di verde SOLO il testo con class="prezzo", lasciando gli altri paragrafi del colore di default:

<p>Descrizione del prodotto.</p>
<p class="prezzo">29,99 €</p>
<p>Spedizione gratuita.</p>

Esercizio w203

Nel seguente CSS, spiega (a parole, in un commento) quale regola vincerà per un elemento <p id="intro" class="testo"> e perché:

p { color: black; }
.testo { color: blue; }
#intro { color: green; }

Esercizio w204

Crea un <div> con width: 150px, padding: 15px, border: 3px solid navy e nessun box-sizing. Calcola (a mano, scrivilo in un commento) quanto è largo in totale l’elemento sulla pagina. Poi aggiungi box-sizing: border-box e ricalcola.

CSS Flexbox

Finora abbiamo visto come colorare e spaziare singoli elementi. Ora affrontiamo il problema del layout: come disporre più elementi uno accanto all’altro, centrarli, distribuirli nello spazio disponibile.

Flexbox (Flexible Box Layout) è pensato esattamente per questo: disporre elementi in una riga o in una colonna, con controllo su allineamento e distribuzione dello spazio.

Note

Flexbox vs Grid

Flexbox e Grid sono due metodi di layout diversi: Flexbox è più semplice e flessibile e adatto a layout in una sola dimensione (riga o colonna), mentre Grid è più complicato e potente e adatto a layout bidimensionale (righe e colonne).

Nel nostro corso introduttivo, parlare di Flexbox basta e avanza… eventuali layout bidimensionali saranno affrontati come composizione di layout Flexbox.

Attivare Flexbox

Flexbox si attiva su un contenitore, e influenza il comportamento dei suoi figli diretti:

menu.html
<nav class="menu">
    <a href="#">Home</a>
    <a href="#">Chi siamo</a>
    <a href="#">Contatti</a>
</nav>
stile.css
.menu {
    display: flex;
}

Con una sola riga di CSS, i tre link — che di default sarebbero elementi in linea — si dispongono ordinatamente su una riga, uno accanto all’altro. Il contenitore (.menu) si dice flex container, i suoi figli diretti (i tre <a>) sono i flex item(s).

Tip

Pensa a display: flex come a un “modo di disporre” che passi al contenitore: è il contenitore a decidere come organizzare i suoi figli, un po’ come una funzione che decide come disporre gli elementi di una lista che le viene passata.

Direzione: riga o colonna

.menu {
    display: flex;
    flex-direction: row; /* valore di default: elementi in riga */
}

.scheda {
    display: flex;
    flex-direction: column; /* elementi impilati in colonna */
}

Allineamento: le due proprietà chiave

Le due proprietà più usate si applicano al contenitore, non ai singoli elementi:

.menu {
    display: flex;
    justify-content: space-between; /* asse principale (orizzontale, se row) */
    align-items: center;            /* asse trasverso (verticale, se row) */
}

justify-content controlla la distribuzione lungo la direzione principale (orizzontale se flex-direction: row):

justify-content: flex-start;    /* elementi ammassati all'inizio: sinistra o alto (default) */
justify-content: flex-end;      /* elementi ammassati alla fine: destra o basso */
justify-content: center;        /* elementi centrati */
justify-content: space-between; /* primo e ultimo ai bordi, spazio uniforme fra gli altri */
justify-content: space-around;  /* spazio uniforme intorno a ogni elemento */
justify-content: space-evenly;  /* spazio identico intorno ad ogni elemento */

align-items controlla l’allineamento sull’asse trasversale (verticale se flex-direction: row, orizzontale se… capito no?):

align-items: normal;     /* elementi non modificati (default) */
align-items: stretch;    /* elementi allungati per riempire la direzione trasversale */
align-items: center;     /* elementi centrati trasversalmente */
align-items: flex-start; /* elementi allineati all'inizio in senso trasversale */
align-items: flex-end;   /* elementi allineati alla fine in senso trasversale */
align-items: baseline;   /* elementi allineati alla base del contenitore */

Tip

Il modo più semplice per ricordarsi le due proprietà:

  • justify-content lavora “nella stessa direzione” della riga/colonna (avanti-indietro),
  • align-items lavora “trasversalmente” rispetto ad essa.

Andare a capo: flex-wrap

Di default, Flexbox prova a mettere tutti gli elementi su una riga sola, anche a costo di stringerli troppo. Per farli andare a capo quando non c’è spazio:

.menu {
    display: flex;
    flex-wrap: wrap;
}

Esercizi

Esercizio w221

Crea 3 <div> dentro un contenitore con display: flex. Disponili in riga, centrati sia orizzontalmente (justify-content: center) sia verticalmente (align-items: center), dando al contenitore un’altezza di almeno 200px per vedere l’effetto.


Esercizio w222

Prendi lo stesso contenitore dell’esercizio precedente e cambia flex-direction in column. Osserva come cambiano gli effetti di justify-content e align-items (cosa controllavano prima in orizzontale, ora controllano in verticale).


Esercizio w223

Crea un <nav> con 5 link. Usando Flexbox, distribuiscili con justify-content: space-between, poi prova space-around e osserva la differenza. Scrivi in un commento CSS quale useresti per un menu di navigazione reale, e perché.


Esercizio w224

Nel seguente CSS manca display: flex sul contenitore, quindi le proprietà justify-content e align-items non hanno alcun effetto. Correggi il problema:

da_correggere.css
.contenitore {
    justify-content: center;
    align-items: center;
    height: 300px;
}

CSS Responsive

Finora abbiamo scritto CSS che si applica sempre, indipendentemente dalla dimensione dello schermo. Ma un menu che sta bene su un monitor da 24 pollici spesso diventa scomodo su un telefono. Le media query permettono di applicare regole CSS diverse a seconda della larghezza dello schermo.

Note

In un epoca in cui si sviluppa ogni cosa pensando al web, al desktop e al mobile in maniera convergente (ovvero si cerca sempre di far coincidere il più possibile il codice delle applicazioni web, mobile e desktop) la logica con cui si modifica il comportamento della nostra applicazione è basata su un unico parametro: la larghezza dello schermo!!!

Media Query

La sintassi di base è molto semplice:

@media (max-width: 600px) {
    /* regole valide SOLO quando lo schermo è largo 600px o meno */
}

Tutto ciò che scrivi dentro le graffe della media query si applica solo se la condizione è vera. Fuori da quella condizione, valgono le regole “normali” scritte altrove nel foglio di stile.

Riprendiamo l’header con menu del capitolo precedente:

.menu {
    display: flex;
    gap: 20px;
}

@media (max-width: 600px) {
    .menu {
        flex-direction: column;
        gap: 10px;
    }
}

Su uno schermo largo, il layout resta quello del capitolo 6: titolo a sinistra, menu a destra. Sotto i 600px di larghezza (tipicamente un telefono), le stesse regole flex-direction: column che abbiamo già imparato impilano tutto verticalmente, uno sotto l’altro.

Warning

non abbiamo scritto CSS nuovo, solo le proprietà Flexbox che già conoscevamo, applicate condizionalmente. Le media query non sono un linguaggio a parte: sono un “se” che racchiude normali regole CSS.

Tip

Sostanzialmente è come un if in Python: se la condizione è vera si esegue il codice dentro il blocco è normale codice, cambia solo quando viene eseguito. Qui cambia a quali larghezze si applica.

viewport meta tag

Perché le media query funzionino correttamente su telefono, serve una riga nell’HTML, nel <head>:

<meta name="viewport" content="width=device-width, initial-scale=1">

Senza questa riga, molti browser mobili mostrano la pagina “rimpicciolita” come se fosse su un monitor largo, e le media query non scattano mai come ci si aspetta. Va messa in ogni pagina, sempre.

Mobile-first o desktop-first?

Ci sono due approcci comuni:

/* Approccio "desktop-first": si parte dal layout grande, poi si corregge per schermi piccoli */
.menu {
    flex-direction: row;
}

@media (max-width: 600px) {
    .menu {
        flex-direction: column;
    }
}
/* Approccio "mobile-first": si parte dal layout piccolo, poi si arricchisce per schermi grandi */
.menu {
    flex-direction: column;
}

@media (min-width: 601px) {
    .menu {
        flex-direction: row;
    }
}

Per le dispense useremo l’approccio desktop-first (max-width) perché è più immediato da leggere quando si parte da un layout già scritto, come i nostri esempi.

Breakpoint comuni

Non esiste un numero “giusto” in assoluto, ma alcuni valori sono diventati convenzione:

@media (max-width: 600px) { /* smartphone */ }
@media (max-width: 900px) { /* tablet */ }

Per le nostre dispense basta un solo breakpoint a 600px: è sufficiente a distinguere “schermo piccolo” da “schermo normale” senza complicare troppo i primi esempi.

Esercizi

Esercizio w221

Aggiungi il meta tag viewport a una pagina HTML che non lo ha ancora.


Esercizio w222

Prendi l’header con menu del Capitolo 6 (Esercizio 6005) e aggiungi una media query che, sotto i 600px, impili titolo e menu in colonna (come nell’esempio di questo capitolo).


Esercizio w223

Nel seguente CSS la media query è scritta con min-width invece di max-width, quindi la colonna si attiva sugli schermi grandi invece che su quelli piccoli. Correggi il problema (mantenendo l’approccio desktop-first):

da_correggere.css
.menu {
    display: flex;
    flex-direction: row;
}

@media (min-width: 600px) {
    .menu {
        flex-direction: column;
    }
}

Esercizio w224

Aggiungi alla pagina del Progetto 1 (la scheda prodotto) un menu di navigazione in stile Capitolo 6, poi rendilo responsive: in colonna sotto i 600px, in riga sopra.

CSS: variabili

Immagina di aver usato lo stesso colore blu in 15 punti diversi del tuo CSS. Se un giorno vuoi cambiarlo, devi trovarli e modificarli tutti. Le variabili CSS (dette anche custom properties) risolvono questo problema: definisci il valore una volta sola, e lo richiami ovunque serva.

stile.css
:root {
    --colore-principale: navy;
    --colore-sfondo: white;
    --spaziatura: 20px;
}

.menu a {
    color: var(--colore-principale);
}

.intestazione {
    background-color: var(--colore-sfondo);
    padding: var(--spaziatura);
}

:root è un selettore speciale che punta all’elemento radice del documento (di fatto, l’intera pagina): definire le variabili lì le rende disponibili ovunque. Il nome deve iniziare sempre con --, e si richiama con var(--nome-variabile).

Il vantaggio di usare le variabili CSS

:root {
    --colore-testo: black;
    --colore-sfondo: white;
}

body {
    color: var(--colore-testo);
    background-color: var(--colore-sfondo);
}

.menu {
    background-color: var(--colore-sfondo);
}

Se ridefinisci --colore-sfondo e --colore-testo in un solo punto, ogni regola che li usa cambia di conseguenza. Questo è esattamente il meccanismo che si usa ad esempio quando si vogliono fornire più temi (es: chiaro/scuro) per la stessa pagina.

Temi CSS

L’idea: definiamo due gruppi di variabili — uno per il tema chiaro (il default) e uno per il tema scuro — e usiamo un <input type="checkbox"> nascosto come “interruttore”, sfruttando la pseudo-classe :checked.

<body>
    <input type="checkbox" id="tema-scuro" class="interruttore-tema">
    <label for="tema-scuro">Tema scuro</label>

    <header class="intestazione">
        <h1>Il mio sito</h1>
    </header>
</body>
:root {
    --colore-testo: black;
    --colore-sfondo: white;
}

/* Quando la checkbox è selezionata, ridefiniamo le variabili su :root */
body:has(.interruttore-tema:checked) {
    --colore-testo: white;
    --colore-sfondo: black;
}

body {
    color: var(--colore-testo);
    background-color: var(--colore-sfondo);
    transition: color 0.3s, background-color 0.3s;
}

Tip

il codice CSS transition: color 0.3s, background-color 0.3s rappresenta una animazione CSS e rende il cambio di tema una dissolvenza morbida invece di uno scatto secco.

Non è necessario, ma secondo me è molto carino ☺️

Il punto chiave è il selettore body:has(.interruttore-tema:checked): la pseudo-classe :checked seleziona la checkbox solo quando è spuntata, la funzione :has() è… autoesplicativa ☺️

Note

Questa tecnica è totalmente “CSS-only”: non serve una riga di JavaScript. Più avanti vedremo come rifare lo stesso identico interruttore usando JS con addEventListener. In questo modo spero di rendere evidente dove può arrivare il CSS e dove invece serve un linguaggio di programmazione web lato client!

Nascondere la checkbox, mostrare solo la label

Nell’esempio sopra la checkbox è visibile e “brutta”. Di solito si nasconde e si stilizza solo la <label> come se fosse un pulsante:

.interruttore-tema {
    position: absolute;
    opacity: 0;
    /* la checkbox esiste ancora e riceve i click, ma è invisibile */
}

.interruttore-tema + label {
    cursor: pointer;
    padding: 8px 12px;
    border: 1px solid var(--colore-testo);
    border-radius: 4px;
}

+ è il combinatore “fratello immediatamente successivo”: seleziona la <label> che viene subito dopo la checkbox nell’HTML.

Esercizi

Esercizio w231

Definisci in :root tre variabili: --colore-titolo, --colore-sfondo, --spaziatura-generale. Usale per colorare un <h1>, lo sfondo del <body>, e il padding di un <div> a scelta.


Esercizio w232

Prendi il codice dell’esempio “Tema chiaro/scuro con solo CSS” e aggiungi una terza variabile, --colore-bordo, con un valore diverso nei due temi. Usala per colorare il border dell’header.


Esercizio w233

Nel seguente CSS il tema scuro non si attiva mai: manca la pseudo-classe :checked nel selettore. Correggi il problema:

:root {
    --colore-sfondo: white;
}

.interruttore-tema ~ * {
    --colore-sfondo: #222;
}

body {
    background-color: var(--colore-sfondo);
}

Esercizio w234

Nella pagina della scheda prodotto (Progetto 1), definisci almeno 3 variabili CSS in :root (colore principale, colore di sfondo, colore del testo) e usale per stilizzare header, sfondo e testo della pagina — senza ancora aggiungere il toggle tema, solo le variabili.

Progetto finale: Step 2.

Riprendi la pagina HTML del Progetto 1 (la scheda prodotto) e miglioriamola grazie a quello che hai imparato in questo blocco.

Consegna

  • Collega un foglio di stile esterno (niente CSS inline o <style> nell’head)
  • Usa Flexbox per almeno due layout della pagina: ad esempio l’header (titolo a sinistra, un eventuale menu o link a destra) e la disposizione di lista + tabella
  • Rendi la pagina responsive: almeno un blocco che passa da riga a colonna sotto i 600px (media query), come fatto per il menu nei capitoli 6-7
  • Aggiungi un toggle tema chiaro/scuro CSS-only (checkbox nascosta + :checked + variabili CSS)
  • Usa variabili CSS in :root per almeno colore principale, colore di sfondo e colore del testo, ridefinite nel tema scuro

Vincoli

  • Zero JavaScript in questo progetto (il toggle deve funzionare solo con CSS)
  • box-sizing: border-box impostato globalmente
  • Il codice HTML del Progetto 1 non dovrebbe essere modificato se non per aggiungere la checkbox del tema e le classi necessarie allo stile.

JavaScript: variabili, tipi, operatori

Con HTML e CSS abbiamo definito struttura e aspetto di una pagina. Manca il comportamento: reagire a un click, validare un form, modificare la pagina senza ricaricarla. Questo è il compito di JavaScript.

Dove va il codice JS

<body>
    <h1>La mia pagina</h1>

    <script src="script.js"></script>
</body>

Come per il CSS, il codice JS va tenuto in un file separato (script.js), collegato con <script src="...">. Va messo alla fine del <body>, appena prima della chiusura: così, quando lo script viene eseguito, tutta la pagina HTML sopra di lui è già stata caricata dal browser!!

Tip

Anche in JavaScript è possibile inserire codice “inline” (<button onclick="...">) o dentro un tag <script> inserito nella sezione head. Evitiamo di farlo per motivi di semplicità, rilettura e semplicità di riutilizzo.

Variabili: let e const

let eta = 17;
let nome = "Andrea";

const PI = 3.14159;
  • let per variabili che possono cambiare valore
  • const per valori che non cambiano mai dopo l’assegnazione iniziale
let punteggio = 0;
punteggio = punteggio + 10; // ok, "let" lo permette

const MAX_TENTATIVI = 3;
MAX_TENTATIVI = 5; // ERRORE: non si può riassegnare una const

A differenza di Python, qui la dichiarazione (let/const) è obbligatoria: non puoi scrivere semplicemente eta = 17!!!
Se lo fai, JavaScript potrebbe “funzionare” comunque in alcuni casi particolarmente banali, ma si andrebbe a creare una variabile globale, invece di una locale alla tua pagina.
Non è una buona pratica, soprattutto da quando si naviga spesso con molte schede aperte! Evitiamolo!!!

Note

Nelle vecchie versioni di JavaScript (prima di let/const), la dichiarazione delle variabili era facoltativa e andava fatta con la clausola var, non distinguendo tra variabili locali e globali, come tra variabili e costanti.
La clausola var è ancora supportata per motivi di compatibilità ma non va usata!!!
Vi ho detto della sua esistenza perchè capiterà di leggere codice online datato che la usa…

Tipi di dato principali

let intero = 42;
let decimale = 3.14;
let testo = "Ciao";
let vero_falso = true;
let niente = null;
let non_definito;

Alcune differenze rispetto a Python:

PythonJavaScript
int, floatun solo tipo number per entrambi
strstring
bool (True/False)boolean (true/false, minuscolo)
Nonenull (assenza di valore voluta)
undefined (variabile non ancora assegnata)
due concetti distinti

Per sapere il tipo di una variabile:

console.log(typeof intero);      // "number"
console.log(typeof testo);       // "string"
console.log(typeof vero_falso);  // "boolean"

Tip

console.log() è l’equivalente di print() in Python: stampa nella console del browser (si apre con F12, scheda Console) — non nella pagina.

Stringhe e template literals

let nome = "Andrea";
let eta = 30;

// Concatenazione classica
let messaggio1 = "Ciao " + nome + ", hai " + eta + " anni";

// Template literal: più leggibile, con backtick e ${}
let messaggio2 = `Ciao ${nome}, hai ${eta} anni`;

I template literal (tra backtick `, non virgolette) permettono di inserire variabili direttamente nel testo con ${...}. E’ la stessa idea delle f-string di Python (f"Ciao {nome}"), sintassi diversa.

Operatori

// Aritmetici: identici a Python
let somma = 5 + 3;
let resto = 10 % 3;

// Confronto: attenzione a == vs ===
console.log(5 == "5");   // true  — confronta i valori, converte i tipi
console.log(5 === "5");  // false — confronta anche il tipo, nessuna conversione

// Logici
let a = true && false; // AND
let b = true || false; // OR
let c = !true;          // NOT

La differenza == vs === non ha un vero equivalente in Python (dove 5 == "5" è semplicemente False, senza ambiguità). Regola pratica: usa sempre === e !==, mai ==/!=. Evita completamente le conversioni automatiche di tipo che possono creare bug difficili da individuare.

Gli operatori logici &&, ||, ! corrispondono esattamente ad and, or, not in Python — cambia solo il simbolo.

Array

Un array è una collezione ordinata di valori, identificata da un unico nome — l’equivalente delle liste Python:

const frutti = ["mela", "pera", "banana"];

console.log(frutti[0]);      // "mela" — si accede per indice partendo da 0, come in Python
console.log(frutti.length);  // 3 — equivalente a len(frutti) in Python

Alcune operazioni comuni:

const numeri = [10, 20, 30];

numeri.push(40);        // aggiunge in fondo: [10, 20, 30, 40]
numeri[0] = 99;          // modifica un elemento: [99, 20, 30, 40]
console.log(numeri[1]);  // 20

push() è l’equivalente di .append() in Python. Un array può contenere qualunque tipo di dato, anche misto:

const misto = ["testo", 42, true];

Nel prossimo capitolo vedremo come scorrere un array elemento per elemento senza dover scrivere frutti[0], frutti[1], frutti[2] a mano.

Esercizi

Esercizio w301

Dichiara tre variabili con let: il tuo nome (stringa), la tua età (numero), se sei studente o no (booleano). Stampale tutte con console.log().


Esercizio w302

Usando un template literal, crea un messaggio che dica "[nome] ha [età] anni" usando le variabili dell’esercizio precedente.


Esercizio w303

Scrivi il risultato (senza eseguire il codice, solo ragionando) di ciascuna riga, poi verifica con console.log():

console.log(3 === "3");
console.log(3 == "3");
console.log(true && false);
console.log(!false);

Esercizio w304

Nel seguente codice c’è un errore: si tenta di riassegnare una const. Correggilo scegliendo tra let e const in modo appropriato per ciascuna variabile:

da_correggere.js
const nome = "Marco";
const punteggio = 0;

punteggio = punteggio + 5;
console.log(`${nome} ha ${punteggio} punti`);

Esercizio w305

Crea un array colori con almeno 4 stringhe. Stampa il primo elemento, l’ultimo elemento (usando colori.length), e la lunghezza totale dell’array.

JS: Condizioni e cicli

L’istruzione if / else

let eta = 17;

if (eta >= 18) {
    console.log("Maggiorenne");
} else {
    console.log("Minorenne");
}

Stessa logica di Python, con due differenze di sintassi:

  • la condizione va tra parentesi ( )
  • il blocco va tra graffe { } (niente : e niente indentazione obbligatoria — anche se restare indentati resta buona pratica)

Per più condizioni in sequenza, else if (equivalente all’elif di Python):

let voto = 7;

if (voto >= 9) {
    console.log("Ottimo");
} else if (voto >= 6) {
    console.log("Sufficiente");
} else {
    console.log("Insufficiente");
}

Condizioni composte

let eta = 20;
let haPatente = true;

if (eta >= 18 && haPatente) {
    console.log("Può guidare");
}

if (eta < 18 || !haPatente) {
    console.log("Non può guidare");
}

Valori “falsy” e “truthy”

Una particolarità di JS da conoscere: dentro un if, alcuni valori si comportano come false anche se non lo sono esplicitamente:

if (0) { }          // falsy
if ("") { }         // falsy (stringa vuota)
if (null) { }       // falsy
if (undefined) { }  // falsy

if (1) { }          // truthy
if ("ciao") { }     // truthy (stringa non vuota)
if ([]) { }         // truthy — attenzione: un array vuoto è truthy! (diverso da Python)

Warning

In Python, if []: è False (una lista vuota è “falsa”). In JS, if ([]) {} è invece vero: un array, anche vuoto, è sempre truthy.

È una delle poche trappole reali per chi passa da Python a JS — tenetela a mente.

Il ciclo for

for (let i = 0; i < 5; i++) {
    console.log(i); // stampa 0, 1, 2, 3, 4
}

Le tre parti tra parentesi, separate da ;:

  1. let i = 0 — inizializzazione, eseguita una sola volta all’inizio
  2. i < 5 — condizione, controllata prima di ogni giro: se falsa, il ciclo si ferma
  3. i++ — incremento, eseguito alla fine di ogni giro (i++ equivale a i = i + 1)

Non esiste un range() diretto come in Python: il ciclo for “classico” di JS descrive esplicitamente le tre parti che in Python sono nascoste dentro range(5). È più verboso, ma anche più flessibile (potete far crescere i di 2 in 2, farlo scendere, ecc. semplicemente cambiando la terza parte).

Il ciclo for…of

Per scorrere gli elementi di un array c’è una forma più diretta, equivalente al for elemento in lista di Python:

const frutti = ["mela", "pera", "banana"];

for (const frutto of frutti) {
    console.log(frutto);
}

Il ciclo while

let tentativi = 0;

while (tentativi < 3) {
    console.log(`Tentativo numero ${tentativi}`);
    tentativi++;
}

Identico a Python nella logica: si ripete finché la condizione resta vera.

Warning

Attenzione a non dimenticare tentativi++ (o un equivalente incremento) dentro il ciclo: senza, la condizione resta vera per sempre e si crea un ciclo infinito

Esercizi

Esercizio w311

Scrivi un if / else che, data una variabile numero, stampi "Positivo", "Negativo" o "Zero" a seconda del suo valore.


Esercizio w312

Usando un ciclo for classico, stampa in console i numeri da 1 a 10.


Esercizio w313

Crea un array di 5 nomi a tua scelta. Usando for...of, stampa per ognuno il messaggio "Ciao, [nome]!".


Esercizio w314

Nel seguente codice il ciclo non si ferma mai (manca l’incremento della variabile). Correggi il problema:

da_correggere.js
let contatore = 0;

while (contatore < 5) {
    console.log(contatore);
}

JS: Funzioni ed eventi

Funzioni

function saluta(nome) {
    return `Ciao, ${nome}!`;
}

let messaggio = saluta("Andrea");
console.log(messaggio); // "Ciao, Andrea!"

La sintassi ricorda Python (def diventa function, e serve la parola chiave return — identica in entrambi i linguaggi), con due differenze da notare:

function saluta(nome) {
    return `Ciao, ${nome}!`;
}
  • le graffe { } delimitano il corpo della funzione (niente indentazione obbligatoria come in Python — anche se restare indentati resta buona pratica per leggibilità)
  • i parametri non hanno un tipo dichiarato né un default “alla Python” con = diretto nella firma in tutti i casi semplici, ma JS supporta comunque valori di default:
function saluta(nome = "ospite") {
    return `Ciao, ${nome}!`;
}

console.log(saluta());        // "Ciao, ospite!"
console.log(saluta("Marco"));  // "Ciao, Marco!"

Funzioni anonime e arrow function

Oltre alla sintassi con function, JS offre una scrittura più compatta molto usata, specialmente per funzioni brevi passate come argomento ad altre funzioni:

// Sintassi "arrow function"
const somma = (a, b) => {
    return a + b;
};

// Se il corpo è una sola riga con return, si può abbreviare ancora:
const somma2 = (a, b) => a + b;

console.log(somma(2, 3));  // 5
console.log(somma2(2, 3)); // 5

Non è un tipo di funzione “diverso” concettualmente: è solo una sintassi più corta, molto comune quando la funzione è breve o viene passata come parametro (lo vedremo subito con gli eventi).

Eventi: far reagire la pagina

Finora il nostro JS eseguiva codice “una volta”, al caricamento della pagina. Gli eventi permettono di eseguire codice in risposta a un’azione dell’utente: un click, la pressione di un tasto, l’invio di un form.

<button id="mio-bottone">Clicca qui</button>

<script src="script.js"></script>
const bottone = document.getElementById("mio-bottone");

bottone.addEventListener("click", function() {
    console.log("Hai cliccato il bottone!");
});

Tre passaggi, sempre gli stessi:

  1. Trovare l’elemento HTML (document.getElementById(...) — lo approfondiamo nel capitolo sul DOM)
  2. Chiamare addEventListener(tipo_evento, funzione) su quell’elemento
  3. Scrivere la funzione che deve eseguirsi quando l’evento scatta (detta callback, cioè “funzione richiamata dopo”)

Con la sintassi arrow function, lo stesso codice si scrive spesso così:

bottone.addEventListener("click", () => {
    console.log("Hai cliccato il bottone!");
});

Note

addEventListener è analogo ai Bind di wxPython, con la possibilità ulteriore di poter definire al volo la funzione evento da eseguire in risposta.
Piccola differenza di nomenclatura: in Python le funzioni collegabili con Bind sono chiamate funzioni evento, in JS le funzioni abbinate ad addEventListener sono chiamate callback.

Altri eventi comuni

// Al passaggio del mouse
elemento.addEventListener("mouseover", () => {
    console.log("Mouse sopra l'elemento");
});

// Alla pressione di un tasto
document.addEventListener("keydown", (evento) => {
    console.log(`Hai premuto: ${evento.key}`);
});

// Quando la pagina ha finito di caricare
document.addEventListener("DOMContentLoaded", () => {
    console.log("Pagina pronta");
});

Nota (evento) nell’esempio di keydown: il browser passa automaticamente alla funzione callback un oggetto con informazioni sull’evento accaduto (quale tasto, quale elemento, eccetera). Lo useremo molto nel capitolo sulla validazione dei form.

DOMContentLoaded è un evento speciale, molto usato: garantisce che il codice al suo interno parta solo dopo che tutto l’HTML è stato caricato — un’alternativa a metter lo <script> in fondo al <body>, utile soprattutto se lo script è nell’<head>.

Esercizi

Esercizio w321

Scrivi una funzione raddoppia(numero) che ritorna il doppio del numero passato. Chiamala con un paio di valori diversi e stampa i risultati con console.log().


Esercizio w322

Riscrivi la funzione dell’esercizio precedente come arrow function in forma abbreviata (una sola riga, senza return esplicito).


Esercizio w323

Crea un bottone HTML con id="saluto". In JS, aggiungi un addEventListener sul click che stampi in console "Ciao dal bottone!".


Esercizio w324

Nel seguente codice l’evento non scatta mai: manca la chiamata a addEventListener, ed è stata scritta erroneamente la chiamata diretta alla funzione. Correggi il problema:

da_correggere.js
const bottone = document.getElementById("mio-bottone");

function mostraMessaggio() {
    console.log("Hai cliccato!");
}

bottone.mostraMessaggio();

JS: Il DOM

Il DOM (Document Object Model) è la rappresentazione della pagina HTML che il browser mantiene in memoria mentre la pagina è aperta. JavaScript non “legge il file HTML”: legge e modifica il DOM — e il browser ridisegna la pagina di conseguenza. Capire questo distingue chi “usa” JS a memoria da chi capisce cosa sta facendo davvero.

Selezionare elementi

pagina.html
<h1 id="titolo">Benvenuti</h1>
<p class="testo">Primo paragrafo</p>
<p class="testo">Secondo paragrafo</p>
script.js
// Un elemento per id (restituisce UN elemento, o null se non esiste)
const titolo = document.getElementById("titolo");

// Tutti gli elementi che soddisfano un selettore CSS (restituisce una lista)
const paragrafi = document.querySelectorAll(".testo");

// Un solo elemento, il primo che soddisfa il selettore
const primoParagrafo = document.querySelector(".testo");

querySelectorAll accetta qualunque selettore CSS — esattamente la stessa sintassi del CSS: .classe, #id, nav > li, p.testo!

Tip

querySelectorAll ritorna una struttura simile a una lista Python, ma non è esattamente una lista (si chiama NodeList):
Per ora, sappi solo che for...of funziona sempre.

const paragrafi = document.querySelectorAll(".testo");

for (const p of paragrafi) {
    console.log(p.textContent);
}

Leggere e modificare il contenuto

const titolo = document.getElementById("titolo");

console.log(titolo.textContent); // legge il testo: "Benvenuti"

titolo.textContent = "Ciao a tutti!"; // modifica il testo

textContent legge o scrive il testo puro di un elemento. Esiste anche innerHTML, che legge/scrive interpretando i tag HTML al suo interno — utile in certi casi, ma va usato con cautela: se il testo che inserisci arriva da un utente (es. un campo di input), innerHTML può eseguire codice non voluto. Per testo semplice, preferite sempre textContent.

Modificare attributi e classi

const immagine = document.querySelector("img");

immagine.setAttribute("src", "nuova_foto.jpg");
immagine.setAttribute("alt", "Nuova descrizione");

// Leggere un attributo
console.log(immagine.getAttribute("src"));

Per le classi CSS esiste una proprietà dedicata, classList, più comoda di setAttribute("class", ...):

const scheda = document.querySelector(".scheda");

scheda.classList.add("evidenziata");     // aggiunge una classe
scheda.classList.remove("evidenziata");  // la rimuove
scheda.classList.toggle("evidenziata");  // la aggiunge se assente, la rimuove se presente

classList.toggle(...) è particolarmente utile: è esattamente il meccanismo con cui si costruisce un interruttore (mostra/nascondi, tema chiaro/scuro, menu aperto/chiuso) — molto più diretto che scrivere if / else per decidere se aggiungere o togliere una classe.

Modificare lo stile direttamente

const titolo = document.getElementById("titolo");

titolo.style.color = "navy";
titolo.style.fontSize = "32px"; // notare: font-size diventa fontSize (camelCase)

Utile per cambiamenti puntuali via JS, ma non è la strada preferita per gli stili “stabili”: quelli restano nel .css. style.* da JS lo userete soprattutto per cambiamenti dinamici legati a un evento (es. cambiare colore al click), non per lo stile di base della pagina.

Creare e rimuovere elementi

const lista = document.querySelector("ul");

// Creare un nuovo elemento
const nuovoElemento = document.createElement("li");
nuovoElemento.textContent = "Nuovo elemento";

// Aggiungerlo alla pagina
lista.appendChild(nuovoElemento);

// Rimuovere un elemento
nuovoElemento.remove();

Il tema chiaro/scuro, versione JavaScript

Riprendiamo l’esempio del tema CSS-only con la checkbox nascosta e vediamo la versione “con logica”:

pagina.html
<button id="toggle-tema">Cambia tema</button>
stile.css
body {
    background-color: white;
    color: black;
    transition: background-color 0.3s, color 0.3s;
}

body.scuro {
    background-color: #222;
    color: white;
}
script.js
const bottone = document.getElementById("toggle-tema");

bottone.addEventListener("click", () => {
    document.body.classList.toggle("scuro");
});

Confrontatelo con la versione CSS-only: lì il “cervello” dell’interruttore era tutto nel CSS (:checked + ~); qui il cervello è nel JS (addEventListener + classList.toggle), e il CSS si limita a definire come appare la classe .scuro, senza sapere quando viene applicata.

È una distinzione che tornerà spesso: CSS descrive l’aspetto, JS decide quando cambiarlo.

Esercizi

Esercizio w331

Crea una pagina con un <h1 id="titolo"> e un bottone. Al click del bottone, cambia il testo del titolo con textContent.


Esercizio w332

Crea 3 <p class="riga"> nella pagina. Usando querySelectorAll e un ciclo for...of, stampa in console il testo di ognuno.


Esercizio w333

Crea un <div class="box"> con un po’ di stile CSS di base, e un bottone. Al click, usa classList.toggle per aggiungere/rimuovere una classe evidenziato che cambi colore di sfondo del box.


Esercizio w334

Nel seguente codice il tema non cambia mai: si usa innerHTML per aggiungere del testo invece di classList.toggle per cambiare classe. Correggi il problema in modo che il click aggiunga/rimuova la classe scuro sul <body>:

da_correggere.js
const bottone = document.getElementById("toggle-tema");

bottone.addEventListener("click", () => {
    document.body.innerHTML += "scuro";
});

JS: Form e validazione lato client

Abbiamo visto nei form HTML come si possa usare l’attributo required per una validazione minima, gestita interamente dal browser. Ora vediamo come intercettare l’invio di un form con JavaScript, per controlli più precisi e messaggi d’errore personalizzati.

Intercettare l’invio di un form

form.html
<form id="form-contatto">
    <label for="nome">Nome:</label>
    <input type="text" id="nome" name="nome">

    <label for="email">Email:</label>
    <input type="email" id="email" name="email">

    <button type="submit">Invia</button>
</form>

<p id="errore"></p>
script.js
const form = document.getElementById("form-contatto");

form.addEventListener("submit", (evento) => {
    evento.preventDefault();
    console.log("Invio intercettato!");
});

evento.preventDefault() è la riga chiave: senza di essa, il browser invierebbe comunque il form e ricaricherebbe la pagina, come da comportamento predefinito. Chiamandola, prendiamo il controllo: decidiamo noi cosa succede, prima di lasciare che il form venga davvero inviato (o bloccarlo del tutto, se i dati non sono validi).

Leggere i valori dei campi

const form = document.getElementById("form-contatto");

form.addEventListener("submit", (evento) => {
    evento.preventDefault();

    const nome = document.getElementById("nome").value;
    const email = document.getElementById("email").value;

    console.log(`Nome: ${nome}, Email: ${email}`);
});

.value legge il contenuto attuale di un <input> — non .textContent (quello serve per il testo dentro un tag, mentre .value legge cosa l’utente ha scritto in un campo di input).

Validare e mostrare un errore

script.js
const form = document.getElementById("form-contatto");
const errore = document.getElementById("errore");

form.addEventListener("submit", (evento) => {
    evento.preventDefault();

    const nome = document.getElementById("nome").value;
    const email = document.getElementById("email").value;

    if (nome === "") {
        errore.textContent = "Il nome è obbligatorio";
        return;
    }

    if (!email.includes("@")) {
        errore.textContent = "Email non valida";
        return;
    }

    errore.textContent = "";
    console.log("Form valido, invio in corso...");
    form.submit(); // ora invia davvero, dopo aver superato i controlli
});

Punti da notare:

  • return dentro la callback dell’evento interrompe la funzione, esattamente come in una funzione qualsiasi: se il nome è vuoto, il codice si ferma lì e non arriva mai a controllare l’email.
  • email.includes("@") è una verifica volutamente semplice (una vera validazione email è più complessa) — sufficiente per capire il meccanismo.
  • Se tutti i controlli passano, si azzera il messaggio d’errore (errore.textContent = "") e si procede con form.submit(), che invia il form per davvero, come se preventDefault() non ci fosse mai stato.

Validare più campi con un ciclo

Se i campi obbligatori sono tanti, si può evitare di ripetere lo stesso controllo copiato e incollato:

form.addEventListener("submit", (evento) => {
    evento.preventDefault();

    const campiObbligatori = document.querySelectorAll("[required]");
    let tuttoValido = true;

    for (const campo of campiObbligatori) {
        if (campo.value === "") {
            campo.style.borderColor = "red";
            tuttoValido = false;
        } else {
            campo.style.borderColor = "";
        }
    }

    if (tuttoValido) {
        console.log("Tutti i campi obbligatori sono compilati");
    }
});

Qui document.querySelectorAll("[required]") seleziona tutti gli elementi che hanno l’attributo required nell’HTML (un selettore CSS “per attributo”, stessa famiglia vista nei capitoli sul CSS) — e il ciclo for...of controlla ognuno con la stessa logica, senza ripetere codice per ogni singolo campo.

Riepilogo: dove sta cosa

CompitoChi se ne occupa
Bloccare l’invio finché non si è prontievento.preventDefault()
Leggere cosa ha scritto l’utente.value
Decidere se un valore è validoif (la tua logica)
Mostrare un messaggio d’erroretextContent su un elemento dedicato
Evidenziare visivamente un campo errato.style o classList

Esercizi

Esercizio w341

Crea un form con un solo campo di testo (nome) e un bottone di invio. Intercetta il submit, usa preventDefault(), e stampa in console il valore scritto dall’utente.


Esercizio w342

Aggiungi al form dell’esercizio precedente un controllo: se il campo nome è vuoto, mostra il messaggio "Il nome è obbligatorio" in un <p id="errore">, senza inviare il form.


Esercizio w343

Crea un form con 3 campi di testo, tutti con l’attributo required. Usando querySelectorAll("[required]") e un ciclo, controlla che siano tutti compilati, colorando di rosso il bordo (style.borderColor) di quelli vuoti.


Esercizio w344

Nel seguente codice il form viene sempre inviato, anche se il campo è vuoto: manca il controllo prima di procedere. Correggi il problema aggiungendo la validazione:

da_correggere.js
const form = document.getElementById("form-contatto");

form.addEventListener("submit", (evento) => {
    evento.preventDefault();

    const nome = document.getElementById("nome").value;

    console.log("Invio in corso...");
    form.submit();
});

Progetto finale: Step 3

Ultimo passaggio: riprendi la scheda prodotto del Progetto 2 (già strutturata in HTML, stilizzata in CSS con Flexbox, responsive, e con il toggle tema CSS-only) e aggiungi il comportamento con JavaScript.

Consegna

  • Validazione lato client del form “Richiedi informazioni” (o “Contatta il venditore”): intercetta il submit con preventDefault(), controlla che i campi obbligatori non siano vuoti e che il campo email contenga una @, mostra i messaggi d’errore in un elemento dedicato senza inviare il form finché non è tutto corretto
  • Rifai il toggle tema in JavaScript: sostituisci (o affianca, a tua scelta) il meccanismo CSS-only con un bottone reale e addEventListener + classList.toggle, come visto nel capitolo sul DOM. Confronta mentalmente le due versioni: cosa cambia, cosa resta uguale?
  • Usa almeno un array in JS in un punto a tua scelta della pagina

Vincoli

  • Il file script.js deve essere separato, collegato con <script src="..."> alla fine del <body>
  • Nessun JS inline
  • Il form deve rimanere bloccato (niente invio reale) finché la validazione non è superata

Traguardo

A questo punto la scheda prodotto avrà attraversato tutti e tre i blocchi: struttura (HTML), aspetto (CSS), comportamento (JS) — lo stesso identico documento, arricchito un livello alla volta. È il modo più concreto per capire perché si dice che questi tre linguaggi lavorano insieme, e non sono tre linguaggi scollegati.

Flask

Flask è un framework web scritto in Python. Serve a costruire applicazioni web e API in modo semplice e veloce, senza dover gestire da zero tutta la complessità di un server HTTP.

Esistono framework più grandi e completi, come Django, ma Flask è volutamente minimale: ti dà gli strumenti essenziali e ti lascia libero di aggiungere solo quello che ti serve.

È molto usato per:

  • applicazioni web di piccole e medie dimensioni
  • API REST
  • prototipi e progetti didattici

Setup, primi passi e routing

Flask è un pacchetto disponibile su PyPi come tanti altri prima d’ora. Per gestire un progetto con Flask, andremo ad esplorare un approccio alternativo: Il problema è che le applicazioni Flask eseguite con Thonny non funzionano (sempre) bene!: Thonny cerca di gestire da solo l’esecuzione di Flask, ma non sempre è in grado di gestire tutto quanto accade, generando a volte errori più fastidiosi di quelli che riusciamo a combinare da soli!!!

Vediamo come fare, in maniera molto semplice:

Da Thonny, vai su Strumenti —> Apri shell di sistema e sul terminale digita:

pip install flask

Lascia aperto quel terminale e ogni volta che vuoi eseguire l’applicazione Flask, digita:

python nome_file_principale.py

Tutto qui!

La prima applicazione

Crea un file chiamato app.py nella cartella del progetto e scrivi questo codice:

from flask import Flask

# Crea l'applicazione Flask
app = Flask(__name__)

# Definisce la route principale
@app.route('/')
def home():
    return 'Ciao, Flask!'

# Avvia il server
if __name__ == '__main__':
    app.run(debug=True)

Tip

La funzione app.run() ha diversi parametri opzionali:

  • debug=True: abilita il debug mode, che mostra in console errori dettagliati (default: False).
  • port=<porta>: specifica la porta su cui il server deve ascoltare (default: 5000).
  • host='0.0.0.0': permette l’accesso al server da qualsiasi IP (default: 127.0.0.1).

Avvia l’applicazione dal terminale:

python app.py

Dovresti vedere un output simile a questo:

 * Running on http://127.0.0.1:5000
 * Debug mode: on

Apri il browser e vai all’indirizzo http://127.0.0.1:5000. Vedrai scritto: Ciao, Flask!

Come funziona

Analizziamo il codice riga per riga:

from flask import Flask

Importa la classe Flask dalla libreria.

app = Flask(__name__)

Crea un’istanza dell’applicazione. __name__ è una variabile Python che contiene il nome del modulo corrente — Flask la usa per trovare le risorse del progetto.

@app.route('/')
def home():
    return 'Ciao, Flask!'

Il decorator @app.route('/') dice a Flask: “quando il browser richiede la pagina /, esegui la funzione home. La funzione restituisce il testo che il browser mostrerà.

app.run(debug=True)

Avvia il server. Il parametro debug=True è utile durante lo sviluppo: riavvia automaticamente il server ad ogni modifica del codice e mostra errori dettagliati nel browser.

Warning

debug=True non va mai usato in produzione.

Esercizi semplici su Flask

Esercizio f101

Modifica app.py in modo che la pagina principale mostri il tuo nome e cognome.


Esercizio f102

Aggiungi una seconda funzione che risponda all’indirizzo /saluto e restituisca il testo "Benvenuto nel corso di Flask!".

Tip

Segui lo stesso schema della funzione home, cambiando il path nel decorator e il nome della funzione.


Esercizio f103

Cosa succede se provi ad andare su un indirizzo che non esiste, come http://127.0.0.1:5000/pippo? Prova e descrivi quello che vedi.

Routing e views

Fino ad ora abbiamo definito route statiche (con URL fissi), come / e /saluto. Flask permette però di definire route dinamiche, dove parte dell’URL è una variabile.

@app.route('/utente/<nome>')
def utente(nome):
    return f'Ciao, {nome}!'

Se vai su http://127.0.0.1:5000/utente/Marco, il browser mostrerà: Ciao, Marco!

La parte <nome> nell’URL viene catturata e passata come parametro alla funzione.

Tipi di parametro

Per default il parametro è una stringa, ma puoi specificare un tipo:

@app.route('/prodotto/<int:id>')
def prodotto(id):
    return f'Hai richiesto il prodotto numero {id}'

I tipi disponibili sono:

TipoEsempio URLValore ricevuto
string (default)/utente/Marco'Marco'
int/prodotto/4242
float/prezzo/3.993.99

Metodi HTTP: GET e POST

Ogni richiesta HTTP ha un metodo che indica l’intenzione del client:

  • GET — chiede di ricevere dati (es. aprire una pagina)
  • POST — invia dati al server (es. compilare un form)

Per default, Flask risponde solo alle richieste GET. Per accettare anche il POST, bisogna specificarlo:

from flask import Flask, request

app = Flask(__name__)

@app.route('/dati', methods=['GET', 'POST'])
def dati():
    if request.method == 'POST':
        return 'Hai inviato un POST'
    return 'Hai fatto una richiesta GET'

L’oggetto request contiene tutte le informazioni sulla richiesta in arrivo: il metodo, i dati del form, i parametri URL, e altro ancora.

Parametri GET nell’URL

Oltre ai parametri dinamici nel path, puoi leggere i parametri query string, quelli che appaiono dopo il ? nell’URL:

http://127.0.0.1:5000/cerca?q=flask
@app.route('/cerca')
def cerca():
    termine = request.args.get('q', '')
    return f'Stai cercando: {termine}'

request.args.get('q', '') legge il parametro q dall’URL. Il secondo argomento ('') è il valore di default se il parametro non è presente.

Dati inviati con POST

Quando un form HTML invia dati con il metodo POST, questi arrivano nel corpo della richiesta e si leggono con request.form:

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        utente = request.form.get('utente')
        password = request.form.get('password')
        return f'Accesso come: {utente}'
    return 'Pagina di login (GET)'

Note

I template HTML per i form li vedremo nel prossimo capitolo. Per ora basta sapere come Flask riceve i dati.

Restituire codici di stato HTTP

Ogni risposta HTTP ha un codice di stato. Flask restituisce 200 OK per default, ma puoi specificarne uno diverso:

@app.route('/non-trovato')
def non_trovato():
    return 'Pagina non trovata', 404

I codici più comuni che incontrerai:

CodiceSignificato
200OK — tutto bene
201Created — risorsa creata
400Bad Request — richiesta malformata
404Not Found — risorsa non trovata
500Internal Server Error — errore del server

Esercizi

Esercizio f111

Crea una route /saluta/<nome> che restituisca "Ciao, [nome]! Benvenuto nel corso.".


Esercizio f112

Crea una route /somma/<int:a>/<int:b> che restituisca il risultato della somma dei due numeri.

Tip

Ricorda che puoi usare f-string per costruire la risposta.


Esercizio f113

Crea una route /cerca che legga un parametro q dalla query string e restituisca "Risultati per: [q]". Se il parametro non è presente, restituisca "Nessun termine di ricerca" con codice di stato 400.

Jinja2 e template(s)

I template

Finora le nostre funzioni restituivano semplici stringhe di testo. In una vera applicazione web vogliamo restituire pagine HTML complete. Potremmo costruire l’HTML direttamente in Python:

@app.route('/')
def home():
    return '<html><body><h1>Ciao!</h1></body></html>'

Funziona, ma diventa presto ingestibile. I template risolvono questo problema: sono file HTML separati, con la possibilità di inserire dati dinamici al loro interno.

Flask usa il motore di template Jinja2, che è già incluso nell’installazione di Flask.

Struttura delle cartelle

Flask si aspetta che i template siano nella cartella templates, nella stessa directory di app.py:

progettoFlask/
├── app.py
└── templates/
    └── index.html

Crea la cartella templates e al suo interno il file index.html:

<!DOCTYPE html>
<html lang="it">
<head>
    <title>La mia prima pagina</title>
</head>
<body>
    <h1>Ciao, Flask!</h1>
</body>
</html>

Per restituire questo template da Flask si usa la funzione render_template:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('index.html')

Flask troverà automaticamente il file nella cartella templates.

Passare dati al template

Il vantaggio dei template è che possiamo passare dati da Python all’HTML. Si usa il secondo argomento di render_template:

@app.route('/')
def home():
    return render_template('index.html', nome='Marco', eta=17)

Modifica il body del file HTML in questo modo: le variabili si usano con la sintassi {{ variabile }}:

<body>
    <h1>Ciao, {{ nome }}!</h1>
    <p>Hai {{ eta }} anni.</p>
</body>

Possiamo passare anche strutture dati più complesse, come liste e dizionari:

@app.route('/')
def home():
    studenti = ['Alice', 'Bruno', 'Carla']
    return render_template('index.html', studenti=studenti)

Esercizi

Esercizio f121

Crea un template profilo.html e una route /profilo che passi al template il tuo nome, la tua città e un numero a piacere. Mostra queste informazioni in una pagina HTML ben formattata.


Esercizio f122

Applicazione con due template(s): la pagina iniziale ha due link: “/saluta/Matteo” e “/saluta/Giovanna”. La pagina saluta raccoglie il secondo parametro del link e lo visualizza scrivendo: ciao <nome>!


Logica nei template

Condizioni

Jinja2 permette di inserire istruzioni condizionali nel template con la sintassi {% %}:

{% if eta >= 18 %}
    <p>Sei maggiorenne.</p>
{% else %}
    <p>Sei minorenne.</p>
{% endif %}

Nota i due tipi di delimitatori:

  • {{ }} — per visualizzare il valore di una variabile
  • {% %} — per istruzioni come if, for, extends…

Cicli

Per scorrere una lista si usa for:

<ul>
{% for studente in studenti %}
    <li>{{ studente }}</li>
{% endfor %}
</ul>

Jinja2 mette a disposizione alcune variabili utili all’interno del ciclo:

{% for studente in studenti %}
    <p>{{ loop.index }} - {{ studente }}</p>
{% endfor %}
VariabileValore
loop.indexIndice corrente (parte da 1)
loop.index0Indice corrente (parte da 0)
loop.firstTrue se è il primo elemento
loop.lastTrue se è l’ultimo elemento

Iterare su dizionari

Possiamo passare anche dizionari e iterare sulle loro chiavi e valori:

@app.route('/prodotto')
def prodotto():
    info = {
        'nome': 'Zaino',
        'prezzo': 29.90,
        'disponibile': True
    }
    return render_template('prodotto.html', info=info)
<dl>
{% for chiave, valore in info.items() %}
    <dt>{{ chiave }}</dt>
    <dd>{{ valore }}</dd>
{% endfor %}
</dl>

Oppure accedendo direttamente ai campi:

<h2>{{ info.nome }}</h2>
<p>Prezzo: {{ info.prezzo }} €</p>
{% if info.disponibile %}
    <p>Disponibile</p>
{% else %}
    <p>Non disponibile</p>
{% endif %}

Esercizi

Esercizio f131

Crea una route /classe che passi al template una lista di almeno 5 nomi di studenti. Il template deve mostrarli in una lista numerata usando loop.index.


Esercizio f132

Modifica l’esercizio precedente: evidenzia in grassetto il primo e l’ultimo elemento della lista usando loop.first e loop.last.

Tip

usa {% if loop.first or loop.last %} per applicare un tag <strong>.


Esercizio f133

Crea una route /voti che passi al template un dizionario con almeno 4 materie e il relativo voto (es. {'Matematica': 8, 'Italiano': 7, ...}). Il template deve mostrare le materie e i voti in una tabella HTML, colorando in rosso i voti inferiori a 6.

Tip

per il colore puoi usare uno stile inline: style="color: red" dentro un {% if %}.


Ereditarietà dei template

Immagina di lavorare ad un sito con dieci pagine, ognuna con la stessa navbar e lo stesso footer. Se vuoi modificare la navbar, devi aprire dieci file. L’ereditarietà dei template risolve questo problema.

Il template base

Si crea un file base.html che contiene la struttura comune a tutte le pagine. Le parti che cambieranno da pagina a pagina vengono definite come blocchi:

<!DOCTYPE html>
<html lang="it">
<head>
    <title>{% block titolo %}Il mio sito{% endblock %}</title>
</head>
<body>

    <nav>
        <a href="/">Home</a> |
        <a href="/about">Chi siamo</a>
    </nav>

    <main>
        {% block contenuto %}{% endblock %}
    </main>

    <footer>
        <p>© 2026 Progetto Flask</p>
    </footer>

</body>
</html>

Il blocco {% block nome %}{% endblock %} è un segnaposto: ogni pagina figlia potrà riempirlo con il proprio contenuto.

Le pagine figlie

Una pagina figlia estende il template base e sovrascrive solo i blocchi che le interessano:

{% extends 'base.html' %}

{% block titolo %}Home{% endblock %}

{% block contenuto %}
    <h1>Benvenuto!</h1>
    <p>Questa è la pagina principale.</p>
{% endblock %}

Tutto il resto — navbar, footer, struttura HTML — viene ereditato automaticamente da base.html.

Un altro esempio per la pagina “Chi siamo”:

{% extends 'base.html' %}

{% block titolo %}Chi siamo{% endblock %}

{% block contenuto %}
    <h1>Chi siamo</h1>
    <p>Questo è il corso di Informatica per le quinte di Scienze Applicate.</p>
{% endblock %}

Struttura finale delle cartelle

progettoFlask/
├── app.py
└── templates/
    ├── base.html
    ├── index.html
    ├── about.html
    └── ...

Esercizi

Esercizio f141 Crea un template base.html con una navbar che contenga i link a / e /about. Crea poi due pagine figlie index.html e about.html che estendano il base e abbiano contenuti diversi. Collega tutto in app.py con le relative route.


Esercizio f142 Aggiungi al template base un terzo blocco chiamato {% block sottotitolo %} posizionato sotto il titolo principale. Nella pagina index.html inserisci un sottotitolo, nella pagina about.html lascialo vuoto (non sovrascrivere il blocco) e verifica che non compaia nulla.

Form e flash messages

Un form HTML permette all’utente di inserire dati e inviarli al server. La struttura di base è:

<form action="/destinazione" method="POST">
    <input type="text" name="nome">
    <button type="submit">Invia</button>
</form>

Gli attributi fondamentali del tag <form>:

  • action — l’URL a cui inviare i dati
  • method — il metodo HTTP da usare (GET o POST)

L’attributo name di ogni campo è la chiave con cui Flask leggerà il valore inviato.

Un form completo

Creiamo un esempio concreto: un form di contatto. Prima il template contatto.html:

{% extends 'base.html' %}

{% block titolo %}Contatti{% endblock %}

{% block contenuto %}
<h1>Scrivici</h1>

<form action="/contatto" method="POST">
    <label for="nome">Nome:</label><br>
    <input type="text" id="nome" name="nome"><br><br>

    <label for="messaggio">Messaggio:</label><br>
    <textarea id="messaggio" name="messaggio" rows="4"></textarea><br><br>

    <button type="submit">Invia</button>
</form>
{% endblock %}

Poi la route in app.py:

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/contatto', methods=['GET', 'POST'])
def contatto():
    if request.method == 'POST':
        nome = request.form.get('nome')
        messaggio = request.form.get('messaggio')
        return f'Grazie {nome}, abbiamo ricevuto il tuo messaggio!'
    return render_template('contatto.html')

Quando l’utente apre /contatto con GET, vede il form. Quando lo compila e clicca “Invia”, il browser fa una richiesta POST alla stessa URL e Flask elabora i dati.

Validazione lato server

Non possiamo fidarci ciecamente dei dati inviati dall’utente. È importante validare che i campi siano compilati e che i valori abbiano senso.

@app.route('/contatto', methods=['GET', 'POST'])
def contatto():
    if request.method == 'POST':
        nome = request.form.get('nome', '').strip()
        messaggio = request.form.get('messaggio', '').strip()

        if not nome:
            return 'Errore: il campo nome è obbligatorio', 400
        if not messaggio:
            return 'Errore: il campo messaggio è obbligatorio', 400

        return f'Grazie {nome}, abbiamo ricevuto il tuo messaggio!'

    return render_template('contatto.html')

Il metodo .strip() rimuove gli spazi iniziali e finali — utile per evitare che un campo con solo spazi venga considerato compilato.

Restituire un semplice testo di errore funziona, ma non è elegante. Nel prossimo paragrafo vedremo un modo migliore per gestire il feedback.

Esercizi

Esercizio f151

Crea un form con i campi nome, cognome ed email. Quando l’utente invia il form, mostra una pagina di conferma con i dati inseriti.


Esercizio f152

Aggiungi la validazione all’esercizio precedente: tutti i campi sono obbligatori. Se manca qualcosa, restituisci un messaggio di errore con codice 400.


Esercizio f153

Aggiungi un campo età di tipo numerico. Verifica che il valore inserito sia un intero compreso tra 10 e 99. In caso contrario, restituisci un errore appropriato.

Tip

usa int() per convertire il valore, e gestisci l’eccezione ValueError nel caso in cui l’utente inserisca un testo non numerico.


Flash messages

La gestione degli errori con return 'Errore...', 400 è rozza: l’utente vede una pagina bianca con il testo dell’errore e deve tornare indietro con il browser. Vorremmo invece:

  • mostrare un messaggio di feedback sulla stessa pagina
  • che il messaggio scompaia dopo essere stato letto

I flash messages di Flask risolvono esattamente questo.

Come funzionano i flash messages

I flash messages si basano sulla sessione di Flask: un messaggio viene salvato temporaneamente, mostrato una volta, e poi eliminato automaticamente.

Per usarli servono due cose:

  1. Una secret key nell’applicazione (necessaria per le sessioni)
  2. Importare flash e redirect
from flask import Flask, render_template, request, flash, redirect

app = Flask(__name__)
app.secret_key = 'chiave_super_segretissima'

Warning

La secret_key serve a Flask per firmare i dati di sessione. In produzione deve essere una stringa lunga e casuale, mai hardcoded nel codice.

Inviare un flash message

Si usa la funzione flash() seguita da un redirect:

@app.route('/contatto', methods=['GET', 'POST'])
def contatto():
    if request.method == 'POST':
        nome = request.form.get('nome', '').strip()
        messaggio = request.form.get('messaggio', '').strip()

        if not nome or not messaggio:
            flash('Tutti i campi sono obbligatori.', 'errore')
            return redirect( '/contatto' )

        flash(f'Grazie {nome}, messaggio ricevuto!', 'successo')
        return redirect( '/contatto' )

    return render_template('contatto.html')

flash(testo, categoria) accetta due argomenti:

  • il testo del messaggio
  • la categoria (una stringa libera che usiamo per lo stile: 'successo', 'errore', 'info'…)

redirect( '/contatto' ) reindirizza l’utente alla route /contatto con una nuova richiesta GET — in questo modo, se l’utente ricarica la pagina, il form non viene reinviato.

Mostrare i flash messages nel template

I messaggi vanno mostrati nel template con get_flashed_messages(). Il posto migliore per definirli è nel base.html, così sono disponibili in tutte le pagine:

{% with messaggi = get_flashed_messages(with_categories=true) %}
    {% if messaggi %}
        {% for categoria, testo in messaggi %}
            <div class="messaggio {{ categoria }}">
                {{ testo }}
            </div>
        {% endfor %}
    {% endif %}
{% endwith %}

E un po’ di CSS per distinguere i tipi:

<style>
    .messaggio {
        padding: 10px;
        margin: 10px;
        border-radius: 5px;
    }
    .successo { background-color: #d4edda; color: #155724; }
    .errore   { background-color: #f8d7da; color: #721c24; }
    .info     { background-color: #d1ecf1; color: #0c5460; }
</style>

Il pattern POST → redirect → GET

Nell’esempio sopra abbiamo seguito un pattern importante: dopo ogni POST, Flask esegue sempre un redirect. Questo si chiama PRG pattern (Post/Redirect/Get) e risolve un problema classico: se l’utente ricarica la pagina dopo aver inviato un form, il browser non reinvia i dati.

1. Utente compila il form  →  richiesta POST
2. Flask elabora i dati    →  redirect (302)
3. Browser segue il redirect  →  richiesta GET
4. Flask mostra la pagina con il flash message

Esercizi

Esercizio f161

Riprendi il form dell’Esercizio 1.1 e sostituisci la gestione degli errori con i flash messages. Usa la categoria 'errore' per i campi mancanti e 'successo' per la conferma.


Esercizio f162

Crea un form con un solo campo numero. L’utente inserisce un numero intero e Flask risponde con un flash message che dice se il numero è pari o dispari. Se il valore inserito non è un numero intero, mostra un messaggio di errore.


Esercizio f163 (più impegnativo)

Crea una piccola app con un form che permetta di inserire un nome in una lista. I nomi vengono salvati in un file di testo nomi.txt, uno per riga. La pagina mostra sempre la lista aggiornata e un flash message di conferma ogni volta che si aggiunge un nome. Gestisci il caso in cui il nome sia già presente nella lista.

File JSON come storage

Già conosci il formato JSON e il modulo json di Python. In Flask lo usiamo per mantenere i dati tra una richiesta e l’altra, salvandoli su file. Useremo codice tipo il seguente:

import json

# caricare i dati da file
file = open('dati.json', 'r')
contenuto = file.read()
file.close()
dati = json.loads(contenuto)

# ... cose ...

# Salvare i dati su file
file = open('dati.json', 'w')
dati_json = json.dumps(dizionario_dati, indent=2, ensure_ascii=False)
file.write(dati_json)
file.close()

Il parametro indent=2 formatta il JSON in modo leggibile. Il parametro ensure_ascii=False permette di salvare correttamente caratteri accentati.


Il pattern load → modifica → save

Ogni volta che vogliamo aggiornare i dati su file, seguiamo sempre questo schema:

import json

# 1. Leggi il file
file = open('dati.json', 'r')
contenuto = file.read()
file.close()
dizionario_dati = json.loads(contenuto)

# 2. Modifica i dati in memoria
dizionario_dati.append({'nome': 'nuovo elemento'})

# 3. Riscrivi il file
file = open('dati.json', 'w')
dati_json = json.dumps(dizionario_dati, indent=2, ensure_ascii=False)
file.write(dati_json)
file.close()

Due errori sono comuni quando si lavora con file JSON in Flask:

  • FileNotFoundError — il file non esiste ancora (prima esecuzione dell’app)
  • json.JSONDecodeError — il file esiste ma il contenuto non è JSON valido

È buona pratica gestirli sempre insieme.

try:
    file = open('dati.json', 'r')
    contenuto = file.read()
    file.close()
    dizionario_dati = json.loads(contenuto)
except (FileNotFoundError, json.JSONDecodeError):
    dizionario_dati = []  # valore di default

Funzioni di utilità

Quando un’applicazione legge e scrive spesso lo stesso file, conviene creare due funzioni di utilità per non ripetere il codice:

FILE = 'dati.json'

def leggi_dati():
    try:
        file = open(FILE, 'r')
        contenuto = file.read()
        file.close()
        dizionario_dati = json.loads(contenuto)
    except (FileNotFoundError, json.JSONDecodeError):
        dizionario_dati = []  # valore di default
        return dizionario_dati
        
def salva_dati(dati):
    file = open(FILE, 'w')
    dati_json = json.dumps(dati, indent=2, ensure_ascii=False)
    file.write(dati_json)
    file.close()

D’ora in poi nel codice useremo solo leggi_dati() e salva_dati().


Esercizio

Esercizio f171

Scrivi uno script Python (senza Flask) che:

  1. legga una lista di nomi da nomi.json gestendo il caso in cui il file non esista
  2. aggiunga un nome passato da tastiera con input()
  3. salvi la lista aggiornata su file

Eseguilo più volte e verifica che i nomi si accumulino correttamente.


CRUD con Flask e JSON

CRUD è l’acronimo delle quattro operazioni fondamentali sui dati:

OperazioneSignificatoHTTP
CreateCrea un nuovo elementoPOST
ReadLeggi / visualizzaGET
UpdateModifica un elemento esistentePOST
DeleteElimina un elementoPOST / GET

Costruiremo una piccola app per gestire una lista di libri da leggere.


Struttura del progetto

libreria_flask/
├── app.py
├── libri.json          ← creato automaticamente alla prima aggiunta
└── templates/
    ├── base.html
    ├── index.html
    └── modifica.html

Setup iniziale

from flask import Flask, render_template, request, flash, redirect, url_for
import json

app = Flask(__name__)
app.secret_key = 'chiave_super_segretissima'

FILE = 'libri.json'

def leggi_dati():
    try:
        file = open(FILE, 'r')
        contenuto = file.read()
        file.close()
        dizionario_dati = json.loads(contenuto)
    except (FileNotFoundError, json.JSONDecodeError):
        dizionario_dati = []  # valore di default
        return dizionario_dati
        
def salva_dati(dati):
    file = open(FILE, 'w')
    dati_json = json.dumps(dati, indent=2, ensure_ascii=False)
    file.write(dati_json)
    file.close()

Read e Create

La pagina principale mostra la lista dei libri e un form per aggiungerne uno nuovo:

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        titolo = request.form.get('titolo', '').strip()
        autore = request.form.get('autore', '').strip()

        if not titolo or not autore:
            flash('Titolo e autore sono obbligatori.', 'errore')
            return redirect( '/' )

        libri = leggi_dati()

        # Controlla duplicati
        for l in libri:
            if l['titolo'].lower() == titolo.lower():
                flash('Questo libro è già nella lista.', 'errore')
                return redirect( '/' )

        libri.append({'titolo': titolo, 'autore': autore, 'letto': False})
        salva_dati(libri)

        flash(f'"{titolo}" aggiunto alla lista.', 'successo')
        return redirect( '/' )

    libri = leggi_dati()
    return render_template('index.html', libri=libri)

Il template index.html:

{% extends 'base.html' %}

{% block titolo %}Lista libri{% endblock %}

{% block contenuto %}
<h1>Lista libri</h1>

<form action="/" method="POST">
    <input type="text" name="titolo" placeholder="Titolo">
    <input type="text" name="autore" placeholder="Autore">
    <button type="submit">Aggiungi</button>
</form>

<ul>
{% for libro in libri %}
    <li>
        <strong>{{ libro.titolo }}</strong> — {{ libro.autore }}
        {% if libro.letto %}✅{% else %}📖{% endif %}
        <a href="/elimina/{{ loop.index0 }}">Elimina</a>
        <a href="/modifica/{{ loop.index0 }}">Modifica</a>
    </li>
{% endfor %}
</ul>
{% endblock %}

Delete

@app.route('/elimina/<int:indice>')
def elimina(indice):
    libri = leggi_dati()

    if indice < 0 or indice >= len(libri):
        flash('Libro non trovato.', 'errore')
        return redirect( '/' )
        
    titolo = libri[indice]['titolo']
    libri.pop(indice)
    salva_dati(libri)

    flash(f'"{titolo}" eliminato.', 'successo')
    return redirect( '/' )

Update

Per la modifica usiamo un indice nell’URL e una pagina dedicata:

@app.route('/modifica/<int:indice>', methods=['GET', 'POST'])
def modifica(indice):
    libri = leggi_dati()

    if indice < 0 or indice >= len(libri):
        flash('Libro non trovato.', 'errore')
        return redirect( '/' )
        
    if request.method == 'POST':
        titolo = request.form.get('titolo', '').strip()
        autore = request.form.get('autore', '').strip()
        letto  = request.form.get('letto') == 'on'

        if not titolo or not autore:
            flash('Titolo e autore sono obbligatori.', 'errore')
            return redirect( f'/modifica/{indice}' )
            
        libri[indice] = {'titolo': titolo, 'autore': autore, 'letto': letto}
        salva_dati(libri)

        flash(f'"{titolo}" aggiornato.', 'successo')
        return redirect( '/' )
        
    return render_template('modifica.html', libro=libri[indice], indice=indice)

Il template modifica.html:

{% extends 'base.html' %}

{% block titolo %}Modifica libro{% endblock %}

{% block contenuto %}
<h1>Modifica libro</h1>

<form action="/modifica/{{ indice }}" method="POST">
    <label>Titolo:</label><br>
    <input type="text" name="titolo" value="{{ libro.titolo }}"><br><br>

    <label>Autore:</label><br>
    <input type="text" name="autore" value="{{ libro.autore }}"><br><br>

    <label>
        <input type="checkbox" name="letto" {% if libro.letto %}checked{% endif %}>
        Già letto
    </label><br><br>

    <button type="submit">Salva</button>
    <a href="/">Annulla</a>
</form>
{% endblock %}

Esercizi

Esercizio f181

Esegui l’app completa e verifica che tutte le operazioni CRUD funzionino correttamente. Apri libri.json con un editor di testo dopo ogni operazione e osserva come cambia il contenuto.


Esercizio f182

Aggiungi all’app un campo anno (anno di pubblicazione). Aggiorna il form di aggiunta, il form di modifica e la visualizzazione nella lista. Valida che l’anno sia un numero intero di 4 cifre.


Esercizio f183 (più impegnativo)

Aggiungi una pagina /cerca con un form che permetta di cercare libri per autore. Il risultato deve essere una lista filtrata dei libri che contengono il termine cercato nel campo autore (ricerca case-insensitive). Se non viene trovato nessun risultato, mostra un messaggio appropriato.




API REST

Web app vs API

Fino ad ora Flask ha restituito pagine HTML destinate al browser. Un’API (Application Programming Interface) funziona diversamente: invece di HTML, restituisce dati — tipicamente in formato JSON — che possono essere consumati da qualsiasi client: un’altra applicazione, un’app mobile, uno script Python, un dispositivo IoT.

Il confronto è semplice:

Web appAPI REST
Risponde conHTMLJSON
DestinatarioBrowser umanoAltro software
InterazioneForm, clickRichieste HTTP programmatiche

Le due cose non si escludono: una stessa applicazione Flask può avere sia pagine HTML per l’utente, sia endpoint API per altri client.

I principi REST

REST (Representational State Transfer) è uno stile architetturale per le API. I concetti chiave sono:

  • Tutte le risorse sono identificate da URL (es. /api/libri, /api/libri/3)
  • Tutte le operazioni sulle risorse sono espresse dai metodi HTTP:
MetodoOperazioneEsempioEffetto
GETLeggiGET /api/librilista tutti i libri
GETLeggi unoGET /api/libri/3leggi il libro 3
POSTCreaPOST /api/libriaggiungi un libro
PUTSostituisciPUT /api/libri/3sostituisci il libro 3
DELETEEliminaDELETE /api/libri/3elimina il libro 3

Rispondere con JSON

Flask mette a disposizione la funzione jsonify per restituire dati JSON in modo corretto, ovvero impostando automaticamente l’header Content-Type: application/json:

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/api/saluto')
def saluto():
    return jsonify({'messaggio': 'Ciao dal server!'})

Puoi restituire dizionari, liste, o qualsiasi struttura serializzabile in JSON:

@app.route('/api/numeri')
def numeri():
    return jsonify([1, 2, 3, 4, 5])

Codici di stato

Con jsonify puoi specificare il codice di stato HTTP come secondo valore di ritorno:

@app.route('/api/libri/<int:indice>')
def get_libro(indice):
    libri = leggi_dati()

    if indice < 0 or indice >= len(libri):
        return jsonify({'errore': 'Libro non trovato'}), 404

    return jsonify(libri[indice]), 200

Restituire il codice corretto è importante: i client lo usano per capire se la richiesta è andata a buon fine senza dover interpretare il contenuto della risposta.


API in sola lettura — GET

Costruiamo i primi due endpoint GET per la nostra lista libri:

from flask import Flask, jsonify
import json

app = Flask(__name__)
FILE = 'libri.json'

def leggi_dati():
    try:
        file = open(FILE, 'r')
        contenuto = file.read()
        file.close()
        dizionario_dati = json.loads(contenuto)
    except (FileNotFoundError, json.JSONDecodeError):
        dizionario_dati = []  # valore di default
        return dizionario_dati

# Lista tutti i libri
@app.route('/api/libri', methods=['GET'])
def get_libri():
    return jsonify(leggi_dati()), 200

# Legge un singolo libro per indice
@app.route('/api/libri/<int:indice>', methods=['GET'])
def get_libro(indice):
    libri = leggi_dati()
    if indice < 0 or indice >= len(libri):
        return jsonify({'errore': 'Libro non trovato'}), 404
    return jsonify(libri[indice]), 200

Puoi testare i endpoint GET direttamente dal browser, oppure con il modulo requests di Python:

import requests

r = requests.get('http://127.0.0.1:5000/api/libri')
print(r.status_code)  # 200
print(r.json())       # lista dei libri

r = requests.get('http://127.0.0.1:5000/api/libri/0')
print(r.json())       # primo libro

Esercizi

Esercizio f191

Aggiungi un endpoint GET /api/libri/letti che restituisca solo i libri con letto: true. Se non ce ne sono, restituisci una lista vuota con codice 200.


Esercizio f192

Aggiungi un endpoint GET /api/libri/cerca?autore=... che filtri i libri per autore (ricerca case-insensitive). Se il parametro autore non è presente, restituisci un errore 400 con un messaggio appropriato.


Ricevere dati JSON

Quando un client invia dati JSON nel corpo della richiesta POST, si leggono con request.get_json():

from flask import request

@app.route('/api/libri', methods=['POST'])
def add_libro():
    dati = request.get_json()

    if not dati:
        return jsonify({'errore': 'Dati JSON mancanti'}), 400

    titolo = dati.get('titolo', '').strip()
    autore = dati.get('autore', '').strip()

    if not titolo or not autore:
        return jsonify({'errore': 'Titolo e autore sono obbligatori'}), 400

    libri = leggi_dati()
    libri.append({'titolo': titolo, 'autore': autore, 'letto': False})
    salva_dati(libri)

    return jsonify({'messaggio': f'"{titolo}" aggiunto'}), 201

Il codice 201 Created indica che una nuova risorsa è stata creata con successo.

Per testare il POST con requests:

import requests

r = requests.post(
    'http://127.0.0.1:5000/api/libri',
    json={'titolo': 'Il nome della rosa', 'autore': 'Umberto Eco'}
)
print(r.status_code)  # 201
print(r.json())       # {'messaggio': '"Il nome della rosa" aggiunto'}

DELETE

@app.route('/api/libri/<int:indice>', methods=['DELETE'])
def delete_libro(indice):
    libri = leggi_dati()

    if indice < 0 or indice >= len(libri):
        return jsonify({'errore': 'Libro non trovato'}), 404

    titolo = libri[indice]['titolo']
    libri.pop(indice)
    salva_dati(libri)

    return jsonify({'messaggio': f'"{titolo}" eliminato'}), 200
import requests

r = requests.delete('http://127.0.0.1:5000/api/libri/0')
print(r.status_code)  # 200
print(r.json())       # {'messaggio': '... eliminato'}

PUT — aggiornamento completo

@app.route('/api/libri/<int:indice>', methods=['PUT'])
def update_libro(indice):
    libri = leggi_dati()

    if indice < 0 or indice >= len(libri):
        return jsonify({'errore': 'Libro non trovato'}), 404

    dati = request.get_json()
    if not dati:
        return jsonify({'errore': 'Dati JSON mancanti'}), 400

    titolo = dati.get('titolo', '').strip()
    autore = dati.get('autore', '').strip()
    letto  = dati.get('letto', False)

    if not titolo or not autore:
        return jsonify({'errore': 'Titolo e autore sono obbligatori'}), 400

    libri[indice] = {'titolo': titolo, 'autore': autore, 'letto': letto}
    salva_dati(libri)

    return jsonify({'messaggio': f'"{titolo}" aggiornato'}), 200
import requests

r = requests.put(
    'http://127.0.0.1:5000/api/libri/0',
    json={'titolo': 'Il nome della rosa', 'autore': 'Umberto Eco', 'letto': True}
)
print(r.status_code)  # 200
print(r.json())       # {'messaggio': '... aggiornato'}

Esercizi

Esercizio f201

Crea uno script Python con requests e verifica che i codici di stato siano corretti in ogni caso.


Esercizio f202

Scrivi uno script Python separato client.py che usi il modulo requests per:

  1. aggiungere tre libri tramite POST
  2. leggere la lista completa tramite GET
  3. eliminare il secondo libro tramite DELETE
  4. rileggere la lista per verificare il risultato

Tip

installa requests con pip install requests.

Per inviare JSON usa requests.post(url, json={...}).


Blueprint

Finora tutto il codice sta in app.py. Man mano che l’applicazione cresce, diventa difficile da leggere e mantenere. I Blueprint di Flask permettono di suddividere le route in moduli separati.

Creare un Blueprint

Creiamo un file separato api.py per tutte le route dell’API:

# api.py
from flask import Blueprint, jsonify, request
import json

api = Blueprint('api', __name__)

FILE = 'libri.json'

def leggi_dati():
    try:
        file = open(FILE, 'r')
        contenuto = file.read()
        file.close()
        dizionario_dati = json.loads(contenuto)
    except (FileNotFoundError, json.JSONDecodeError):
        dizionario_dati = []  # valore di default
        return dizionario_dati
        
def salva_dati(dati):
    file = open(FILE, 'w')
    dati_json = json.dumps(dati, indent=2, ensure_ascii=False)
    file.write(dati_json)
    file.close()

@api.route('/libri', methods=['GET'])
def get_libri():
    return jsonify(leggi_dati()), 200

@api.route('/libri/<int:indice>', methods=['GET'])
def get_libro(indice):
    libri = leggi_dati()
    if indice < 0 or indice >= len(libri):
        return jsonify({'errore': 'Libro non trovato'}), 404
    return jsonify(libri[indice]), 200

# ... altri endpoint

4.3 Registrare il Blueprint

In app.py si registra il Blueprint con un prefisso URL:

# app.py
from flask import Flask
from api import api

app = Flask(__name__)
app.secret_key = 'chiave_segreta'

app.register_blueprint(api, url_prefix='/api')

if __name__ == '__main__':
    app.run(debug=True)

Con url_prefix='/api', tutte le route definite nel Blueprint saranno automaticamente precedute da /api. La route /libri nel Blueprint diventa /api/libri nell’applicazione.

La struttura finale del progetto:

corso_flask/
├── app.py
├── api.py
├── libri.json
└── templates/
    ├── base.html
    ├── index.html
    └── modifica.html

Esercizio

Esercizio f211

Riorganizza il progetto usando i Blueprint: sposta tutte le route API in api.py e tutte le route della web app in views.py. Il file app.py deve contenere solo la creazione dell’app e la registrazione dei due Blueprint.


Progetto finale e verifica

In questo capitolo metterai insieme tutto quello che hai imparato:

  • strutturare un progetto Flask completo
  • usare template Jinja2 con ereditarietà
  • gestire form e flash messages
  • salvare e leggere dati su file JSON
  • esporre un’API REST

Progetto finale

Il progetto finale consiste nel costruire una piccola applicazione Flask completa, con interfaccia web e API REST. Puoi scegliere uno dei quattro temi proposti di seguito — la struttura richiesta è la stessa per tutti.

Realizza un’applicazione Flask che soddisfi i seguenti punti. Ogni punto è indipendente: se non riesci a completarne uno, passa al successivo.


Requisiti comuni

Ogni progetto deve includere:

Web app

  • Un template base.html con navbar e footer
  • Una pagina principale che mostra tutti gli elementi
  • Un form per aggiungere un nuovo elemento (con validazione e flash messages)
  • La possibilità di modificare ed eliminare un elemento

Storage

  • I dati devono essere salvati su file JSON
  • Usare le funzioni leggi_dati() e salva_dati() come visto nel Capitolo 4

API REST

  • GET /api/<risorsa> — restituisce tutti gli elementi
  • GET /api/<risorsa>/<indice> — restituisce un singolo elemento
  • POST /api/<risorsa> — aggiunge un nuovo elemento
  • DELETE /api/<risorsa>/<indice> — elimina un elemento

Organizzazione

  • Le route della web app e le route API devono essere in Blueprint separati

Proposta A — 🎬 Watchlist film

Gestisci una lista personale di film.

Ogni film ha:

  • titolo (stringa, obbligatorio)
  • regista (stringa, obbligatorio)
  • anno (intero, obbligatorio)
  • visto (booleano, default false)

Funzionalità aggiuntive suggerite:

  • Filtra la lista per mostrare solo i film visti o solo quelli da vedere
  • Endpoint API GET /api/film/da-vedere che restituisce i film con visto: false

Proposta B — 📋 Bacheca annunci

Pubblica e gestisci annunci.

Ogni annuncio ha:

  • titolo (stringa, obbligatorio)
  • testo (stringa, obbligatorio)
  • autore (stringa, obbligatorio)
  • data (stringa ISO, generata automaticamente con datetime.date.today().isoformat())

Funzionalità aggiuntive suggerite:

  • Gli annunci sono mostrati in ordine dal più recente al più vecchio
  • Endpoint API GET /api/annunci/cerca?q=... che filtra per parola chiave nel titolo o nel testo

Proposta C — 🏆 Classifica torneo

Gestisci la classifica di un torneo.

Ogni giocatore ha:

  • nome (stringa, obbligatorio)
  • punteggio (intero, default 0)
  • partite (intero, default 0)

Funzionalità aggiuntive suggerite:

  • La classifica è sempre mostrata ordinata per punteggio decrescente
  • Endpoint API GET /api/classifica/top?n=3 che restituisce i primi n giocatori

Proposta D — 🍕 Registro recensioni

Tieni traccia di locali visitati.

Ogni recensione ha:

  • nome (stringa, obbligatorio)
  • tipo (stringa, es. "ristorante", "bar", "pizzeria")
  • voto (intero da 1 a 10, obbligatorio)
  • commento (stringa, opzionale)

Funzionalità aggiuntive suggerite:

  • Mostra la media dei voti in cima alla pagina
  • Endpoint API GET /api/locali/top?voto_min=8 che filtra per voto minimo

Struttura consigliata del progetto

progetto/
├── app.py
├── views.py        ← Blueprint web app
├── api.py          ← Blueprint API
├── dati.json       ← creato automaticamente
└── templates/
    ├── base.html
    ├── index.html
    └── modifica.html

Tempo per la prova

100 minuti


Valutazione prova pratica

[20 punti] Web app

  1. Crea il template base.html con navbar e almeno due link di navigazione
  2. Crea la pagina principale che mostra tutti gli elementi letti dal file JSON
  3. Aggiungi un form per inserire un nuovo elemento, con validazione e flash messages
  4. Implementa la funzione di eliminazione di un elemento

[20 punti] Storage JSON

  1. I dati sono salvati e letti correttamente da file JSON
  2. L’applicazione gestisce correttamente il caso in cui il file non esista ancora
  3. Implementa le funzioni leggi_dati() e salva_dati()

[20 punti] API REST

  1. Endpoint GET /api/<risorsa> che restituisce tutti gli elementi in JSON
  2. Endpoint GET /api/<risorsa>/<indice> che restituisce un singolo elemento, con 404 se non trovato
  3. Endpoint POST /api/<risorsa> che aggiunge un elemento ricevuto come JSON, con validazione
  4. Endpoint DELETE /api/<risorsa>/<indice> che elimina un elemento

[10 punti] Organizzazione

  1. Le route web e le route API sono in Blueprint separati

[10 punti] Funzionalità aggiuntiva

  1. Implementa la funzionalità aggiuntiva suggerita per il tema scelto

Autenticazione utenti

In questo capitolo affronteremo i concetti relativi alle sessioni e all’autenticazione in una applicazione gestita con Flask.

Sessioni

HTTP è un protocollo stateless: ogni richiesta è indipendente dalla precedente. Il server non ricorda chi sei tra una richiesta e l’altra.

Le sessioni risolvono questo problema: permettono di salvare informazioni associate a un utente per tutta la durata della sua visita. Flask salva i dati di sessione in un cookie firmato con la secret_key.

from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'chiave_super_segretissima'

@app.route('/set')
def set_sessione():
    session['utente'] = 'Marco'
    return 'Sessione impostata'

@app.route('/get')
def get_sessione():
    utente = session.get('utente', 'nessuno')
    return f'Utente in sessione: {utente}'

@app.route('/clear')
def clear_sessione():
    session.clear()
    return 'Sessione cancellata'

session si comporta come un dizionario Python — puoi leggere, scrivere e cancellare valori. I dati persistono tra una richiesta e l’altra finché il browser non chiude la sessione o finché non la cancelli esplicitamente.

Esercizi

Esercizio f401

Crea una piccola app con tre route: /imposta/<nome> che salva il nome in sessione, /chi-sono che mostra il nome salvato (o “ospite” se non c’è), e /esci che cancella la sessione e reindirizza a /chi-sono.


Flask-Login

Gestire l’autenticazione a mano con le sessioni è possibile, ma richiede di ripetere lo stesso codice in ogni route protetta:

@app.route('/dashboard')
def dashboard():
    if 'utente' not in session:
        return redirect( '/login' )
    return render_template('dashboard.html')

Con molte route protette questo diventa noioso e soggetto a errori. Flask-Login automatizza tutto questo. E’ un modulo ausiliario per Flask, che puoi installare come al solito:

pip install flask-login

Struttura il progetto in questo modo:

progetto/
├── app.py
├── utenti.json
└── templates/
    ├── base.html
    ├── login.html
    └── dashboard.html

Il modello utente

Flask-Login richiede che il modello utente implementi quattro proprietà e un metodo. Il modo più semplice è ereditare da UserMixin, che le implementa tutte con valori di default ragionevoli:

from flask_login import UserMixin

class Utente(UserMixin):
    def __init__(self, id, username, password):
        self.id = id
        self.username = username
        self.password = password

Le proprietà fornite da UserMixin:

  • is_authenticatedTrue se l’utente è loggato
  • is_activeTrue se l’account è attivo
  • is_anonymousTrue se non è loggato
  • get_id() — restituisce l’id come stringa

Setup di Flask-Login

from flask import Flask, render_template, request, redirect, url_for, flash
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
import json

app = Flask(__name__)
app.secret_key = 'chiave_segreta'

# Inizializza Flask-Login
login_manager = LoginManager(app)
login_manager.login_view = 'login'  # route a cui reindirizzare se non loggato
login_manager.login_message = 'Devi effettuare il login per accedere a questa pagina.'
login_manager.login_message_category = 'errore'

login_view è il nome della funzione (non l’URL) a cui Flask-Login reindirizza automaticamente l’utente quando tenta di accedere a una route protetta senza essere loggato.

Gestione utenti su file JSON

Gli utenti sono salvati in utenti.json:

[
    {"id": "1", "username": "admin", "password": "admin123"},
    {"id": "2", "username": "mario", "password": "mario456"}
]

Warning

In questo primo esempio salviamo le password in chiaro per semplicità. In una applicazione reale le password vanno sempre hashate — lo vedremo nella sezione dedicata.

Funzioni di utilità per leggere gli utenti e cercarli:

def leggi_utenti():
    try:
        file = open('utenti.json', 'r')
        users = file.read()
        file.close()
        return json.loads(users)
    except (FileNotFoundError, json.JSONDecodeError):
        return []

def trova_utente_per_id(id):
    for u in leggi_utenti():
        if u['id'] == str(id):
            return Utente(u['id'], u['username'], u['password'])
    return None

def trova_utente_per_username(username):
    for u in leggi_utenti():
        if u['username'] == username:
            return Utente(u['id'], u['username'], u['password'])
    return None

Il user_loader

Flask-Login ha bisogno di sapere come ricaricare un utente dalla sessione ad ogni richiesta. Si definisce con il decorator @login_manager.user_loader:

@login_manager.user_loader
def carica_utente(id):
    return trova_utente_per_id(id)

Flask-Login chiama questa funzione automaticamente ad ogni richiesta, passando l’id salvato in sessione. Se la funzione restituisce None, l’utente viene considerato non loggato.

Login e logout

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('dashboard'))

    if request.method == 'POST':
        username = request.form.get('username', '').strip()
        password = request.form.get('password', '').strip()

        utente = trova_utente_per_username(username)

        if not utente or utente.password != password:
            flash('Credenziali non valide.', 'errore')
            return redirect(url_for('login'))

        login_user(utente)
        flash(f'Benvenuto, {utente.username}!', 'successo')
        return redirect(url_for('dashboard'))

    return render_template('login.html')


@app.route('/logout')
@login_required
def logout():
    logout_user()
    flash('Hai effettuato il logout.', 'info')
    return redirect(url_for('login'))

login_user(utente) salva l’utente in sessione. logout_user() la cancella.

Il template login.html:

{% extends 'base.html' %}

{% block titolo %}Login{% endblock %}

{% block contenuto %}
<h1>Login</h1>

<form action="/login" method="POST">
    <label>Username:</label><br>
    <input type="text" name="username"><br><br>

    <label>Password:</label><br>
    <input type="password" name="password"><br><br>

    <button type="submit">Accedi</button>
</form>
{% endblock %}

Proteggere le route

Il decorator @login_required protegge una route: se l’utente non è loggato, viene reindirizzato automaticamente alla login_view definita in precedenza.

@app.route('/dashboard')
@login_required
def dashboard():
    return render_template('dashboard.html')

Nel template puoi accedere all’utente corrente con la variabile current_user, disponibile automaticamente in tutti i template:

{% extends 'base.html' %}

{% block titolo %}Dashboard{% endblock %}

{% block contenuto %}
<h1>Ciao, {{ current_user.username }}!</h1>
<p>Sei loggato con successo.</p>
<a href="/logout">Esci</a>
{% endblock %}

Hashare le password

Salvare le password in chiaro è una pessima pratica. Con la libreria werkzeug — già inclusa in Flask — possiamo hashare le password facilmente:

from werkzeug.security import generate_password_hash, check_password_hash

# Quando si registra un utente
password_hashata = generate_password_hash('mia_password')

# Quando si verifica il login
ok = check_password_hash(password_hashata, 'mia_password')  # True
ok = check_password_hash(password_hashata, 'password_sbagliata')  # False

Il file utenti.json con password hashate:

[
    {
        "id": "1",
        "username": "admin",
        "password": "pbkdf2:sha256:..."
    }
]

La verifica nel login diventa:

from werkzeug.security import check_password_hash

if not utente or not check_password_hash(utente.password, password):
    flash('Credenziali non valide.', 'errore')
    return redirect(url_for('login'))

Esercizi

Esercizio f411

Completa l’app con Flask-Login: crea il file utenti.json con almeno due utenti, implementa login e logout, e proteggi la route /dashboard. Verifica che un utente non loggato venga reindirizzato al login.


Esercizio f422

Aggiungi una route /profilo protetta che mostri username e id dell’utente corrente usando current_user.


Esercizio f423 (più impegnativo)

Aggiungi una route /registrazione che permetta di creare un nuovo utente. I dati vanno salvati in utenti.json con la password hashata usando generate_password_hash. Gestisci il caso in cui lo username scelto sia già presente.

Upload di file

In questo capitolo parleremo di:

  • gestire l’upload di file tramite form HTML
  • validare il tipo e la dimensione del file
  • salvare i file su disco in modo sicuro
  • servire i file caricati agli utenti

Upload di base

Per permettere l’upload di file, il form HTML deve avere due accorgimenti rispetto a un form normale:

  • enctype="multipart/form-data" — indica al browser di inviare il file come dati binari
  • <input type="file"> — il campo per selezionare il file
<form action="/upload" method="POST" enctype="multipart/form-data">
    <label>Scegli un file:</label><br>
    <input type="file" name="file"><br><br>
    <button type="submit">Carica</button>
</form>

Senza enctype="multipart/form-data" il file non viene inviato correttamente — è l’errore più comune.

Ricevere il file in Flask

Il file caricato si legge con request.files, in modo analogo a request.form per i campi di testo:

from flask import Flask, request, flash, redirect, url_for

app = Flask(__name__)
app.secret_key = 'chiave_segreta'

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        file = request.files.get('file')

        if not file or file.filename == '':
            flash('Nessun file selezionato.', 'errore')
            return redirect(url_for('upload'))

        file.save('uploads/' + file.filename)
        flash(f'File "{file.filename}" caricato con successo.', 'successo')
        return redirect(url_for('upload'))

    return render_template('upload.html')

file.filename contiene il nome originale del file. file.save(percorso) lo salva su disco.

Creare la cartella uploads

La cartella uploads/ deve esistere prima di salvare i file. Puoi crearla automaticamente all’avvio dell’app:

import os

UPLOAD_FOLDER = 'uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

os.makedirs(..., exist_ok=True) crea la cartella se non esiste, senza errori se esiste già.

La route di upload diventa:

file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))

Esercizi

Esercizio f431

Crea una app con una route /upload che permetta di caricare un file qualsiasi. Dopo il caricamento, mostra un flash message di conferma con il nome del file. Verifica che il file sia presente nella cartella uploads/.


Validazione

Accettare qualsiasi file è pericoloso. È buona pratica limitare i tipi di file consentiti verificando l’estensione:

ESTENSIONI_CONSENTITE = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}

def estensione_consentita(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ESTENSIONI_CONSENTITE

rsplit('.', 1) divide il nome del file sull’ultimo punto, restituendo nome ed estensione. Il controllo '.' in filename evita file senza estensione.

La route aggiornata:

if not estensione_consentita(file.filename):
    flash('Tipo di file non consentito.', 'errore')
    return redirect(url_for('upload'))

Nomi sicuri con secure_filename

Il nome del file fornito dall’utente non è affidabile — potrebbe contenere caratteri speciali o percorsi come ../../etc/passwd che potrebbero sovrascrivere file di sistema.

Werkzeug mette a disposizione secure_filename che pulisce il nome del file:

from werkzeug.utils import secure_filename

filename = secure_filename(file.filename)
# 'My File (1).jpg'  →  'My_File_1_.jpg'
# '../../etc/passwd' →  'etc_passwd'

Il salvataggio sicuro diventa:

filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

Evitare sovrascritture

Se due utenti caricano un file con lo stesso nome, il secondo sovrascrive il primo. Una soluzione semplice è aggiungere un prefisso univoco al nome del file usando uuid:

import uuid

def nome_univoco(filename):
    estensione = filename.rsplit('.', 1)[1].lower()
    return f"{uuid.uuid4().hex}.{estensione}"

uuid.uuid4().hex genera una stringa esadecimale casuale di 32 caratteri — praticamente impossibile da indovinare o ripetere.

filename = nome_univoco(secure_filename(file.filename))
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

Limitare la dimensione del file

Flask permette di impostare una dimensione massima per le richieste in ingresso:

app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024  # 2 MB

Se il file supera il limite, Flask solleva automaticamente un errore 413 Request Entity Too Large. Puoi intercettarlo con un handler personalizzato:

from flask import abort
from werkzeug.exceptions import RequestEntityTooLarge

@app.errorhandler(RequestEntityTooLarge)
def file_troppo_grande(e):
    flash('Il file supera la dimensione massima consentita (2 MB).', 'errore')
    return redirect(url_for('upload'))

Esercizi

Esercizio f441

Aggiorna l’app dell’esercizio precedente aggiungendo:

  • validazione dell’estensione (solo immagini: png, jpg, jpeg, gif)
  • nome del file reso sicuro con secure_filename
  • dimensione massima di 1 MB

Esercizio f442

Modifica l’app in modo che ogni file venga salvato con un nome univoco generato con uuid. Salva in uploads.json un registro dei file caricati con il nome originale, il nome salvato su disco e la data di caricamento.


Servire i file e galleria

I file nella cartella uploads/ non sono automaticamente accessibili dal browser. Per servirli bisogna creare una route apposita con send_from_directory:

from flask import send_from_directory

@app.route('/uploads/<filename>')
def file_caricato(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

Ora il file uploads/immagine.jpg è raggiungibile all’URL /uploads/immagine.jpg.

Una galleria immagini completa

Mettiamo tutto insieme: un’app che permette di caricare immagini e le mostra in una galleria.

import os
import uuid
import json
from datetime import date
from flask import Flask, render_template, request, flash, redirect, url_for, send_from_directory
from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge

app = Flask(__name__)
app.secret_key = 'chiave_segreta'
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024

ESTENSIONI_CONSENTITE = {'png', 'jpg', 'jpeg', 'gif'}
REGISTRO = 'uploads.json'

os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

def estensione_consentita(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ESTENSIONI_CONSENTITE

def nome_univoco(filename):
    estensione = filename.rsplit('.', 1)[1].lower()
    return f"{uuid.uuid4().hex}.{estensione}"

def leggi_registro():
    try:
        with open(REGISTRO, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return []

def salva_registro(dati):
    with open(REGISTRO, 'w', encoding='utf-8') as f:
        json.dump(dati, f, indent=2, ensure_ascii=False)

@app.errorhandler(RequestEntityTooLarge)
def file_troppo_grande(e):
    flash('Il file supera la dimensione massima consentita (2 MB).', 'errore')
    return redirect(url_for('galleria'))

@app.route('/', methods=['GET', 'POST'])
def galleria():
    if request.method == 'POST':
        file = request.files.get('file')

        if not file or file.filename == '':
            flash('Nessun file selezionato.', 'errore')
            return redirect(url_for('galleria'))

        if not estensione_consentita(file.filename):
            flash('Tipo di file non consentito.', 'errore')
            return redirect(url_for('galleria'))

        filename_sicuro = secure_filename(file.filename)
        filename_salvato = nome_univoco(filename_sicuro)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename_salvato))

        registro = leggi_registro()
        registro.append({
            'nome_originale': filename_sicuro,
            'nome_salvato': filename_salvato,
            'data': date.today().isoformat()
        })
        salva_registro(registro)

        flash(f'"{filename_sicuro}" caricato con successo.', 'successo')
        return redirect(url_for('galleria'))

    immagini = leggi_registro()
    return render_template('galleria.html', immagini=immagini)

@app.route('/uploads/<filename>')
def file_caricato(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

Il template galleria.html:

{% extends 'base.html' %}

{% block titolo %}Galleria{% endblock %}

{% block contenuto %}
<h1>Galleria immagini</h1>

<form action="/" method="POST" enctype="multipart/form-data">
    <input type="file" name="file">
    <button type="submit">Carica</button>
</form>

<div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px;">
{% for img in immagini %}
    <div>
        <img src="/uploads/{{ img.nome_salvato }}" style="width: 200px; height: 150px; object-fit: cover;">
        <p style="font-size: 0.85em;">{{ img.nome_originale }}<br>{{ img.data }}</p>
    </div>
{% endfor %}
</div>
{% endblock %}

Esercizi

Esercizio f451

Aggiungi alla galleria la possibilità di eliminare un’immagine. La cancellazione deve rimuovere sia il file dalla cartella uploads/ che la voce corrispondente in uploads.json.

Tip

usa os.remove(percorso) per eliminare un file dal disco.


Esercizio f452 (più impegnativo)

Proteggi la route di upload con @login_required usando Flask-Login (vedi Capitolo 1). Solo gli utenti loggati possono caricare immagini, ma tutti possono vedere la galleria.

Struttura avanzata del progetto

In questo capitolo vedremo come:

  • organizzare un progetto Flask in moduli separati
  • usare l’application factory per creare l’app
  • gestire configurazioni diverse per sviluppo e produzione
  • strutturare Blueprint multipli in modo professionale

Il problema della crescita

Fino ad ora tutto il codice stava in app.py. Per progetti piccoli va bene, ma man mano che l’applicazione cresce — login, upload, API, decine di route — un solo file diventa difficile da leggere e mantenere.

Considera un progetto reale con autenticazione e upload:

app.py   ← 300+ righe, tutto mescolato

L’obiettivo è arrivare a qualcosa di simile:

progetto/
├── app.py          ← solo 10 righe: crea e avvia l'app
├── config.py       ← configurazione
├── auth/           ← modulo autenticazione
│   ├── __init__.py
│   └── routes.py
├── galleria/       ← modulo galleria
│   ├── __init__.py
│   └── routes.py
├── static/
└── templates/
    ├── base.html
    ├── auth/
    │   └── login.html
    └── galleria/
        └── galleria.html

Ogni modulo ha le sue route, i suoi template, la sua logica — e app.py si limita ad assemblarli.

Application Factory

L’application factory è una funzione che crea e configura l’app Flask. Invece di creare l’app a livello di modulo — come abbiamo fatto finora — la creiamo dentro una funzione chiamata create_app:

Prima (approccio semplice):

# app.py
app = Flask(__name__)
app.secret_key = 'chiave'
# ... tutto il resto

Dopo (application factory):

# app.py
from factory import create_app

app = create_app()

if __name__ == '__main__':
    app.run(debug=True)
# factory.py
from flask import Flask

def create_app():
    app = Flask(__name__)
    app.secret_key = 'chiave_segreta'
    # registra Blueprint, estensioni, ecc.
    return app

Il vantaggio principale è che possiamo creare istanze diverse dell’app con configurazioni diverse — ad esempio una per lo sviluppo e una per la produzione.

Configurazione separata

Creiamo un file config.py con classi di configurazione:

# config.py
import os

class Config:
    SECRET_KEY = 'chiave_segreta_di_default'
    UPLOAD_FOLDER = 'uploads'
    MAX_CONTENT_LENGTH = 2 * 1024 * 1024  # 2 MB

class DevConfig(Config):
    DEBUG = True

class ProdConfig(Config):
    DEBUG = False
    SECRET_KEY = os.environ.get('SECRET_KEY', 'cambia-questa-chiave')

os.environ.get('SECRET_KEY', ...) legge la chiave da una variabile d’ambiente — in produzione non vogliamo mai avere segreti nel codice sorgente.

La factory sceglie la configurazione giusta:

# factory.py
from flask import Flask
from config import DevConfig, ProdConfig
import os

def create_app():
    app = Flask(__name__)

    # Sceglie la configurazione in base all'ambiente
    if os.environ.get('FLASK_ENV') == 'production':
        app.config.from_object(ProdConfig)
    else:
        app.config.from_object(DevConfig)

    return app

Esercizi

Esercizio f461

Prendi uno dei progetti dei capitoli precedenti e riscrivilo usando l’application factory. Crea factory.py con la funzione create_app e config.py con almeno due classi di configurazione (DevConfig e ProdConfig). Verifica che l’app funzioni esattamente come prima.


Blueprint multipli

Ogni modulo dell’applicazione diventa un pacchetto Python — una cartella con un file __init__.py. Il Blueprint viene definito nel file routes.py del pacchetto.

Creiamo il modulo auth:

auth/
├── __init__.py   ← vuoto o con importazioni
└── routes.py     ← Blueprint con le route
# auth/routes.py
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required

auth = Blueprint('auth', __name__, template_folder='templates')

@auth.route('/login', methods=['GET', 'POST'])
def login():
    # ... logica di login
    return render_template('auth/login.html')

@auth.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('auth.login'))

Nota url_for('auth.login') — quando si usano Blueprint, il nome della route va prefissato con il nome del Blueprint.

Template per Blueprint

Ogni Blueprint può avere la propria sottocartella di template:

templates/
├── base.html
├── auth/
│   ├── login.html
│   └── registrazione.html
└── galleria/
    └── galleria.html

Nei template, i link tra moduli diversi usano il prefisso del Blueprint:

<a href="{{ url_for('auth.login') }}">Login</a>
<a href="{{ url_for('galleria.index') }}">Galleria</a>

Registrare i Blueprint nella factory

# factory.py
from flask import Flask
from flask_login import LoginManager
from config import DevConfig, ProdConfig
import os

def create_app():
    app = Flask(__name__)

    if os.environ.get('FLASK_ENV') == 'production':
        app.config.from_object(ProdConfig)
    else:
        app.config.from_object(DevConfig)

    # Inizializza Flask-Login
    login_manager = LoginManager(app)
    login_manager.login_view = 'auth.login'

    @login_manager.user_loader
    def carica_utente(id):
        from auth.utils import trova_utente_per_id
        return trova_utente_per_id(id)

    # Registra i Blueprint
    from auth.routes import auth
    from galleria.routes import galleria

    app.register_blueprint(auth, url_prefix='/auth')
    app.register_blueprint(galleria, url_prefix='/')

    return app

Struttura finale completa

progetto/
├── app.py
├── factory.py
├── config.py
├── utenti.json
├── uploads.json
├── uploads/
├── auth/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py        ← funzioni di utilità (leggi_utenti, trova_utente...)
├── galleria/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py        ← funzioni di utilità (leggi_registro, salva_registro...)
└── templates/
    ├── base.html
    ├── auth/
    │   ├── login.html
    │   └── registrazione.html
    └── galleria/
        └── galleria.html
# app.py
from factory import create_app

app = create_app()

if __name__ == '__main__':
    app.run()

app.py è ridotto a tre righe. Tutta la logica di configurazione e registrazione è nella factory.

Esercizi

Esercizio f471

Riorganizza il progetto completo (autenticazione + galleria) nella struttura a pacchetti descritta sopra. I moduli auth e galleria devono essere Blueprint separati, ognuno con le proprie route, utility e template.


Esercizio f472

Aggiungi un terzo Blueprint api con un prefisso /api. Per ora basta un endpoint GET /api/immagini che restituisca la lista delle immagini caricate in formato JSON. Registralo nella factory insieme agli altri due.


Esercizio f473 (più impegnativo)

Aggiungi alla factory la gestione delle cartelle necessarie all’avvio: se la cartella uploads/ non esiste, deve essere creata automaticamente. Fai lo stesso per i file JSON (utenti.json, uploads.json) — se non esistono, inizializzali con una lista vuota [].

Tip

aggiungi questa logica in fondo a create_app, prima del return app.

API REST avanzata

In questo capitolo vedremo come:

  • gestire gli errori HTTP in modo centralizzato
  • implementare la paginazione dei risultati
  • proteggere le API con token di autenticazione
  • documentare gli endpoint con una pagina di riferimento

Gestione centralizzata degli errori

Finora quando qualcosa andava storto restituivamo un errore direttamente nella route:

if not dati:
    return jsonify({'errore': 'Dati mancanti'}), 400

In un’API con molti endpoint questo approccio ha due problemi: il formato degli errori rischia di essere inconsistente tra una route e l’altra, e il codice si riempie di controlli ripetuti.

La soluzione è gestire gli errori in modo centralizzato.

Handler di errori globali

Flask permette di registrare handler per i codici di errore HTTP. Questi handler intercettano automaticamente gli errori sollevati ovunque nell’applicazione:

from flask import jsonify

@app.errorhandler(400)
def bad_request(e):
    return jsonify({'errore': 'Richiesta non valida', 'dettaglio': str(e)}), 400

@app.errorhandler(404)
def not_found(e):
    return jsonify({'errore': 'Risorsa non trovata'}), 404

@app.errorhandler(405)
def method_not_allowed(e):
    return jsonify({'errore': 'Metodo non consentito'}), 405

@app.errorhandler(500)
def internal_error(e):
    return jsonify({'errore': 'Errore interno del server'}), 500

Ora tutte le risposte di errore hanno lo stesso formato JSON, indipendentemente da dove viene generato l’errore.

Sollevare errori con abort

Nelle route puoi sollevare un errore HTTP con abort, che verrà intercettato dall’handler corrispondente:

from flask import abort

@app.route('/api/libri/<int:indice>')
def get_libro(indice):
    libri = leggi_dati()
    if indice < 0 or indice >= len(libri):
        abort(404)
    return jsonify(libri[indice]), 200

abort(404) interrompe immediatamente l’esecuzione della route e passa il controllo all’handler del 404. Il codice diventa più leggibile — niente più return jsonify({'errore': ...}) ripetuti ovunque.

Errori personalizzati

Per errori più specifici puoi creare eccezioni personalizzate:

from werkzeug.exceptions import HTTPException

class DatiMancanti(HTTPException):
    code = 400
    description = 'I dati JSON sono mancanti o malformati.'

class RisorsaNonTrovata(HTTPException):
    code = 404
    description = 'La risorsa richiesta non esiste.'

@app.errorhandler(DatiMancanti)
def dati_mancanti(e):
    return jsonify({'errore': e.description}), e.code

Esercizi

Esercizio f481

Aggiungi al progetto gli handler per i codici 400, 404 e 500. Sostituisci tutti i return jsonify({'errore': ...}) nelle route API con abort(codice). Verifica che le risposte di errore abbiano tutte lo stesso formato.


Esercizio f482

Aggiungi un handler per il codice 429 Too Many Requests. Per ora non serve implementare un vero rate limiting — basta che l’handler restituisca un JSON con un messaggio appropriato.


Paginazione

Un endpoint che restituisce tutti i dati in una volta sola funziona finché i dati sono pochi. Con centinaia o migliaia di elementi diventa lento e pesante. La paginazione divide i risultati in pagine di dimensione fissa.

Paginazione con query string

Il modo più semplice è accettare i parametri pagina e per_pagina nella query string:

GET /api/libri?pagina=2&per_pagina=5
@app.route('/api/libri')
def get_libri():
    libri = leggi_libri()

    # Legge i parametri con valori di default
    try:
        pagina = int(request.args.get('pagina', 1))
        per_pagina = int(request.args.get('per_pagina', 5))
    except ValueError:
        abort(400)

    if pagina < 1 or per_pagina < 1:
        abort(400)

    # Calcola gli indici di inizio e fine
    inizio = (pagina - 1) * per_pagina
    fine = inizio + per_pagina

    risultati = libri[inizio:fine]
    totale = len(libri)
    pagine_totali = (totale + per_pagina - 1) // per_pagina

    return jsonify({
        'dati': risultati,
        'paginazione': {
            'pagina': pagina,
            'per_pagina': per_pagina,
            'totale': totale,
            'pagine_totali': pagine_totali,
            'ha_precedente': pagina > 1,
            'ha_successiva': pagina < pagine_totali
        }
    }), 200

La risposta include sia i dati che le informazioni di paginazione — il client sa quante pagine esistono e se può navigare avanti o indietro.

Usare la paginazione con requests

import requests

# Prima pagina
r = requests.get('http://127.0.0.1:5000/api/libri?pagina=1&per_pagina=3')
dati = r.json()

print(f"Pagina {dati['paginazione']['pagina']} di {dati['paginazione']['pagine_totali']}")
for libro in dati['dati']:
    print(f"  - {libro['titolo']}")

# Pagina successiva se esiste
if dati['paginazione']['ha_successiva']:
    r2 = requests.get('http://127.0.0.1:5000/api/libri?pagina=2&per_pagina=3')
    print(r2.json())

Esercizi

Esercizio f491

Aggiungi la paginazione all’endpoint GET /api/libri. Popola libri.json con almeno 15 elementi e verifica il funzionamento con valori diversi di pagina e per_pagina.


Esercizio f492

Scrivi uno script pagina_tutti.py che usi requests per scorrere automaticamente tutte le pagine e stampare tutti i titoli, una pagina alla volta.


Autenticazione con token

Flask-Login funziona bene per le web app, dove il browser gestisce i cookie di sessione automaticamente. Per le API però i cookie non sono pratici — i client come script Python o app mobile preferiscono un sistema più semplice: il token.

Il flusso è:

  1. Il client fa login inviando username e password
  2. Il server verifica le credenziali e restituisce un token — una stringa univoca
  3. Il client include il token in ogni richiesta successiva nell’header Authorization
  4. Il server verifica il token e risponde

Generare e salvare i token

Per semplicità generiamo token casuali con secrets e li salviamo in un file JSON:

import secrets
import json

TOKEN_FILE = 'token.json'

def leggi_token():
    try:
        with open(TOKEN_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return {}

def salva_token(token_db):
    with open(TOKEN_FILE, 'w', encoding='utf-8') as f:
        json.dump(token_db, f, indent=2)

def genera_token():
    return secrets.token_hex(32)

Il file token.json mappa ogni token al relativo username:

{
    "a3f8c2...": "admin",
    "9d1e4b...": "mario"
}

Endpoint di login

from werkzeug.security import check_password_hash

@app.route('/api/login', methods=['POST'])
def api_login():
    dati = request.get_json()
    if not dati:
        abort(400)

    username = dati.get('username', '').strip()
    password = dati.get('password', '').strip()

    utente = trova_utente_per_username(username)
    if not utente or not check_password_hash(utente.password, password):
        return jsonify({'errore': 'Credenziali non valide'}), 401

    # Genera e salva il token
    token = genera_token()
    token_db = leggi_token()
    token_db[token] = username
    salva_token(token_db)

    return jsonify({'token': token, 'utente': username}), 200

Verificare il token

Creiamo una funzione che estrae e verifica il token dall’header della richiesta:

def verifica_token():
    auth_header = request.headers.get('Authorization', '')

    # Il formato atteso è: "Bearer <token>"
    if not auth_header.startswith('Bearer '):
        abort(401)

    token = auth_header[7:]  # Rimuove "Bearer "
    token_db = leggi_token()

    if token not in token_db:
        abort(401)

    return token_db[token]  # Restituisce lo username

Nelle route protette basta chiamare verifica_token() all’inizio:

@app.route('/api/libri', methods=['POST'])
def add_libro():
    username = verifica_token()  # Solleva 401 se non valido

    dati = request.get_json()
    if not dati:
        abort(400)

    # ... resto della logica

Usare il token con requests

import requests

# Login
r = requests.post('http://127.0.0.1:5000/api/login',
    json={'username': 'admin', 'password': 'admin123'})
token = r.json()['token']

# Richiesta autenticata
headers = {'Authorization': f'Bearer {token}'}

r = requests.post('http://127.0.0.1:5000/api/libri',
    json={'titolo': 'Il nome della rosa', 'autore': 'Umberto Eco'},
    headers=headers)
print(r.status_code, r.json())

Logout — invalidare il token

@app.route('/api/logout', methods=['POST'])
def api_logout():
    auth_header = request.headers.get('Authorization', '')
    if auth_header.startswith('Bearer '):
        token = auth_header[7:]
        token_db = leggi_token()
        token_db.pop(token, None)
        salva_token(token_db)
    return jsonify({'messaggio': 'Logout effettuato'}), 200

Esercizi

Esercizio f501

Implementa il sistema di autenticazione con token. Proteggi gli endpoint POST, PUT e DELETE della tua API — l’endpoint GET rimane pubblico. Verifica il funzionamento con uno script requests.


Esercizio f502

Aggiungi un endpoint GET /api/me che restituisce le informazioni dell’utente corrente (username) basandosi sul token. Deve restituire 401 se il token non è presente o non è valido.


Esercizio f503 (più impegnativo)

Aggiungi una scadenza ai token: ogni token ha una data di creazione salvata in token.json. Se il token ha più di 24 ore, viene considerato scaduto e la richiesta viene rifiutata con 401. La funzione verifica_token deve controllare la scadenza.

Tip

Usa datetime.datetime.now().isoformat() per salvare la data di creazione e datetime.datetime.fromisoformat() per rileggerla.

Deploy

In questo capitolo cercheremo di:

  • capire perché il server di sviluppo Flask non va usato in produzione
  • descrivere cos’è uno Web Server Gateway WSGI
  • descrivere cos’è un reverse proxy

Il server di sviluppo non basta

Quando avviamo la nostra app con:

python app.py

Flask avvia il suo server di sviluppo integrato. È comodo durante lo sviluppo — si riavvia automaticamente ad ogni modifica, mostra errori dettagliati nel browser — ma ha limiti importanti che lo rendono inadatto alla produzione:

  • gestisce una sola richiesta alla volta — se due utenti aprono la pagina contemporaneamente, il secondo deve aspettare che il primo abbia finito
  • la modalità debug=True espone informazioni sensibili sul server a chiunque
  • non è progettato per essere stabile sotto carico continuativo
  • Flask stesso avvisa esplicitamente: “Do not use the development server in a production environment”

La catena di produzione

In un ambiente di produzione l’applicazione Flask non viene esposta direttamente a Internet. Davanti ad essa ci sono due componenti aggiuntivi:

Internet → Nginx → Gunicorn → Flask

Ognuno ha un ruolo preciso:

  • Reverse Proxy — riceve le connessioni da Internet, gestisce HTTPS, serve i file statici, e inoltra le richieste dinamiche al web server

  • Web Server Gateway — esegue l’applicazione Flask con più processi paralleli, uno per ogni richiesta simultanea

  • Flask — elabora la richiesta e produce la risposta, esattamente come in sviluppo

Dal punto di vista di Flask non cambia nulla — riceve richieste HTTP e produce risposte HTTP, senza sapere da dove arrivano.


WSGI

Python ha prodotto nel tempo molti framework web: Flask, Django, FastAPI, Bottle e altri. Allo stesso modo esistono molti server web capaci di eseguire applicazioni Python. Senza uno standard comune, ogni framework avrebbe dovuto essere compatibile con ogni server — un problema di integrazione enorme.

WSGI (Web Server Gateway Interface) risolve questo problema: è uno standard Python che definisce un’interfaccia comune tra server web e applicazioni web. Qualsiasi server WSGI può eseguire qualsiasi applicazione WSGI, indipendentemente dal framework usato.

Flask è un’applicazione WSGI. Gunicorn è un server WSGI. Parlano la stessa lingua.


2.2 Server WSGI disponibili

Un server WSGI esegue l’applicazione Flask gestendo più richieste in parallelo attraverso un sistema di worker — processi indipendenti, ognuno dei quali può elaborare una richiesta simultaneamente.

Server WSGI
├── worker 1  ←  elabora la richiesta dell'utente A
├── worker 2  ←  elabora la richiesta dell'utente B
├── worker 3  ←  elabora la richiesta dell'utente C
└── worker 4  ←  in attesa

Con quattro worker, quattro utenti possono essere serviti contemporaneamente. Il server di sviluppo Flask ne gestisce solo uno alla volta.

I server WSGI più diffusi in ambito Python sono:

ServerNote
GunicornSemplice, affidabile, molto usato su Linux
uWSGIMolto configurabile, diffuso su hosting condiviso
WaitressScritto in Python puro, funziona anche su Windows
mod_wsgiModulo per Apache, comune in ambienti enterprise

Una regola pratica comune per il numero di worker è 2 × numero_di_CPU + 1.


Reverse proxy

Un proxy è un intermediario: riceve richieste da un client e le inoltra a un altro server per conto suo. Un proxy diretto (forward proxy) viene usato dai client per accedere a risorse esterne — ad esempio un proxy aziendale che filtra il traffico in uscita.

Un reverse proxy funziona al contrario: si posiziona davanti ai server e riceve richieste da Internet, inoltrandole al server appropriato. Dal punto di vista del client, il reverse proxy è il server — non sa che dietro c’è un’altra macchina.

Client                Reverse proxy         Server interno
──────                ─────────────         ──────────────
browser  →  richiesta → Nginx       → richiesta → Gunicorn+Flask
browser  ←  risposta  ← Nginx       ← risposta  ← Gunicorn+Flask

Reverse proxy disponibili

Usare un reverse proxy davanti al server WSGI aggiunge funzionalità essenziali che quest’ultimo non è progettato per gestire:

Gestione delle connessioni — un reverse proxy è estremamente efficiente nel gestire migliaia di connessioni simultanee, anche lente. Un client lento non occupa un worker per tutta la durata della connessione.

File statici — il reverse proxy serve file CSS, JavaScript e immagini direttamente dal disco, senza coinvolgere Flask. È molto più veloce e scarica lavoro dall’applicazione.

HTTPS — il reverse proxy gestisce i certificati SSL/TLS e cifra le connessioni. Flask riceve sempre richieste HTTP semplici, senza occuparsi della crittografia.

Sicurezza — può limitare la dimensione delle richieste, bloccare IP sospetti, gestire rate limiting. Fa da scudo prima che le richieste raggiungano l’applicazione.

I reverse proxy più diffusi sono:

SoftwareNote
NginxLeggero, performante, molto usato con Flask e Django
ApacheStorico e diffusissimo, configurabile con mod_proxy
CaddyModerno, gestisce HTTPS automaticamente
TraefikPensato per ambienti container e microservizi
HAProxyAltissime prestazioni, eccellente per bilanciamento del carico e alta disponibilità

Il flusso completo

Vediamo cosa succede quando un utente apre la nostra app in produzione:

  1. Il browser invia una richiesta HTTPS al server
  2. Nginx riceve la richiesta, decifra HTTPS e la analizza
  3. Se è una richiesta per un file statico (CSS, immagine), Nginx la serve direttamente dal disco
  4. Se è una richiesta dinamica (una pagina Flask, un endpoint API), Nginx la inoltra a Gunicorn
  5. Gunicorn assegna la richiesta a un worker libero
  6. Il worker esegue la funzione Flask corrispondente e produce la risposta
  7. La risposta risale la catena: Gunicorn → Nginx → browser

Dal punto di vista del codice Flask, i passi 2, 3, 4 e 5 sono completamente trasparenti.


In produzione

Cosa cambia nel codice??

Molto poco. Le differenze principali rispetto allo sviluppo sono:

  • debug=False — niente errori dettagliati esposti al browser
  • La SECRET_KEY viene letta da una variabile d’ambiente, non scritta nel codice
  • I file di log vengono scritti su disco invece che stampati sul terminale

Il codice dell’applicazione — route, template, logica — rimane identico.


Variabili d’ambiente

In produzione i valori sensibili come la SECRET_KEY non vanno nel codice sorgente — potrebbero finire su Git e diventare pubblici. Si usano le variabili d’ambiente: valori impostati nel sistema operativo del server, fuori dal codice.

import os

class ProdConfig:
    DEBUG = False
    SECRET_KEY = os.environ.get('SECRET_KEY')

os.environ.get('SECRET_KEY') legge il valore dalla variabile d’ambiente SECRET_KEY impostata sul server. Se la variabile non esiste, restituisce None.

Progetti finali

I progetti si svolgono a coppie. Il codice va gestito con git e le dipendenze con uv. Al termine della consegna, il docente si occupa del deploy sul server del laboratorio.

Requisiti comuni a tutti i progetti

Struttura

  • Application factory (create_app) in factory.py
  • Configurazione in config.py con DevConfig e ProdConfig
  • Blueprint separati per web app e API
  • Template con ereditarietà da base.html

Codice

  • Dipendenze gestite con uv — file pyproject.toml aggiornato
  • Nessuna secret key nel codice sorgente — va in .env
  • .env presente ma non committato su git (nel .gitignore)
  • debug=False in ProdConfig

Consegna

  • Repository git con storia dei commit significativa — almeno un commit per funzionalità
  • README.md con descrizione del progetto e istruzioni per avviarlo in locale
  • Nessun file __pycache__, .env, o uploads/ nel repository

Progetto A — 🖼️ Galleria condivisa

Descrizione Un’app dove gli utenti registrati caricano immagini che vengono mostrate in una galleria pubblica. Chiunque può vedere le immagini, solo gli utenti loggati possono caricarle ed eliminare le proprie.

Funzionalità richieste

Web app

  • Registrazione e login con password hashata
  • Upload di immagini (solo jpg, png, gif, max 2 MB)
  • Galleria pubblica con nome dell’autore e data di caricamento
  • Ogni utente può eliminare solo le proprie immagini
  • Flash messages per tutte le operazioni

API REST

  • GET /api/immagini — lista di tutte le immagini con paginazione
  • GET /api/immagini/<indice> — dettaglio di una singola immagine
  • DELETE /api/immagini/<indice> — elimina un’immagine (richiede token)

Autenticazione API

  • POST /api/login — restituisce un token
  • Le operazioni di modifica richiedono il token nell’header Authorization: Bearer <token>

Struttura attesa

galleria/
├── factory.py
├── config.py
├── app.py
├── .gitignore
├── README.md
├── pyproject.toml
├── auth/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py
├── galleria/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py
├── api/
│   ├── __init__.py
│   └── routes.py
└── templates/
    ├── base.html
    ├── auth/
    └── galleria/

Progetto B — 📝 Mini blog

Descrizione Un blog multiutente dove gli utenti registrati pubblicano post con titolo, testo e tag. La home mostra tutti i post in ordine cronologico. Chiunque può leggere, solo gli autori possono modificare o eliminare i propri post.

Funzionalità richieste

Web app

  • Registrazione e login con password hashata
  • Creazione di post con titolo, testo e tag (separati da virgola)
  • Home con tutti i post in ordine dal più recente, con autore e data
  • Pagina di dettaglio del singolo post
  • Ogni utente può modificare ed eliminare solo i propri post
  • Filtro per tag: cliccando su un tag si vedono solo i post con quel tag
  • Flash messages per tutte le operazioni

API REST

  • GET /api/post — lista dei post con paginazione
  • GET /api/post/<indice> — dettaglio di un post
  • GET /api/post?tag=flask — filtra i post per tag
  • POST /api/post — crea un nuovo post (richiede token)
  • DELETE /api/post/<indice> — elimina un post (richiede token)

Autenticazione API

  • POST /api/login — restituisce un token
  • Le operazioni di modifica richiedono il token nell’header Authorization: Bearer <token>

Struttura attesa

miniblog/
├── factory.py
├── config.py
├── app.py
├── .gitignore
├── README.md
├── pyproject.toml
├── auth/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py
├── blog/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py
├── api/
│   ├── __init__.py
│   └── routes.py
└── templates/
    ├── base.html
    ├── auth/
    └── blog/

Progetto C — 🗳️ Sondaggi

Descrizione Un’app dove gli utenti registrati creano sondaggi con una domanda e più opzioni di risposta. Chiunque può votare — ma solo una volta per sondaggio, controllato tramite sessione. I risultati sono visibili in tempo reale dopo il voto.

Funzionalità richieste

Web app

  • Registrazione e login con password hashata
  • Creazione di un sondaggio con domanda e almeno due opzioni
  • Pagina di voto: mostra la domanda e le opzioni come radio button
  • Dopo il voto, reindirizza alla pagina dei risultati con le percentuali
  • Un utente non loggato può votare, ma non creare sondaggi
  • Ogni utente può eliminare solo i propri sondaggi
  • Flash messages per tutte le operazioni

Controllo voto unico

  • Il controllo che un utente abbia già votato va fatto tramite sessione
  • Se un utente tenta di votare due volte, viene reindirizzato ai risultati con un messaggio

API REST

  • GET /api/sondaggi — lista di tutti i sondaggi
  • GET /api/sondaggi/<indice> — dettaglio con domanda, opzioni e risultati
  • POST /api/sondaggi — crea un nuovo sondaggio (richiede token)
  • DELETE /api/sondaggi/<indice> — elimina un sondaggio (richiede token)

Autenticazione API

  • POST /api/login — restituisce un token
  • Le operazioni di modifica richiedono il token nell’header Authorization: Bearer <token>

Struttura attesa

sondaggi/
├── factory.py
├── config.py
├── app.py
├── .gitignore
├── README.md
├── pyproject.toml
├── auth/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py
├── sondaggi/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py
├── api/
│   ├── __init__.py
│   └── routes.py
└── templates/
    ├── base.html
    ├── auth/
    └── sondaggi/

Progetto D — 📅 Agenda condivisa

Descrizione Un’agenda dove gli utenti registrati aggiungono eventi con titolo, data, ora e descrizione. Gli eventi sono visibili a tutti in ordine cronologico. Ogni utente gestisce solo i propri eventi.

Funzionalità richieste

Web app

  • Registrazione e login con password hashata
  • Aggiunta di eventi con titolo, data (date), ora (time) e descrizione
  • Home con tutti gli eventi futuri in ordine cronologico, con autore
  • Evidenziazione visiva degli eventi del giorno corrente
  • Ogni utente può modificare ed eliminare solo i propri eventi
  • Flash messages per tutte le operazioni

API REST

  • GET /api/eventi — lista di tutti gli eventi futuri con paginazione
  • GET /api/eventi/<indice> — dettaglio di un evento
  • GET /api/eventi?autore=mario — filtra gli eventi per autore
  • POST /api/eventi — crea un nuovo evento (richiede token)
  • DELETE /api/eventi/<indice> — elimina un evento (richiede token)

Autenticazione API

  • POST /api/login — restituisce un token
  • Le operazioni di modifica richiedono il token nell’header Authorization: Bearer <token>

Struttura attesa

agenda/
├── factory.py
├── config.py
├── app.py
├── .gitignore
├── README.md
├── pyproject.toml
├── auth/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py
├── agenda/
│   ├── __init__.py
│   ├── routes.py
│   └── utils.py
├── api/
│   ├── __init__.py
│   └── routes.py
└── templates/
    ├── base.html
    ├── auth/
    └── agenda/

Buon lavoro!