Programmazione asincrona in Python: la guida completa
Velocizza il tuo codice con la programmazione asincrona in Python. Una guida passo passo ad asyncio, concorrenza, richieste HTTP efficienti e integrazione con database.
Mentre il tuo script Python aspetta pazientemente le risposte delle API, le query al database o il completamento delle operazioni sui file, spesso quel tempo va sprecato. Con la programmazione asincrona in Python, il tuo codice può gestire più attività contemporaneamente. Così, mentre un'operazione è in attesa, le altre vanno avanti, trasformando i momenti morti in lavoro produttivo e riducendo spesso minuti di attesa a pochi secondi.
In questa guida ti insegnerò le basi della programmazione asincrona in Python lavorando su mini-progetti. Vedrai come coroutine, event loop e I/O asincrono possano rendere il tuo codice molto più reattivo.
Se vuoi imparare a creare web API asincrone, dai un'occhiata a questo corso su FastAPI.
Che cos'è la programmazione asincrona in Python?
Nel Python sincrono tradizionale, il codice viene eseguito una riga alla volta. Per esempio, quando chiami un'API, il programma si ferma e aspetta la risposta. Se ci vogliono due secondi, l'intero programma resta inattivo per due secondi. La programmazione asincrona consente al tuo codice di avviare una chiamata API e poi proseguire con altre attività.
Quando arriva la risposta, il codice riprende da dove si era fermato. Invece di aspettare che ogni operazione termini, puoi eseguirne più di una contemporaneamente. Questo è particolarmente importante quando il tuo codice passa tempo ad attendere sistemi esterni come database, API o file system.
Per far funzionare tutto, il sistema async di Python usa alcuni concetti chiave:
Coroutine: funzioni definite con async def invece di def. Possono sospendere e riprendere l'esecuzione, rendendole perfette per operazioni che comportano attese.
await: questa parola chiave dice a Python: "metti in pausa questa coroutine finché questa operazione non termina, ma lascia che nel frattempo giri altro codice".
Event loop: il motore che gestisce tutte le coroutine, decidendo quale eseguire e quando passare da una all'altra.
Task: coroutine incapsulate per l'esecuzione concorrente. Le crei con asyncio.create_task() per eseguire più operazioni in parallelo.
Per evitare confusione su cosa la programmazione async può (e non può) fare, tieni a mente questo:
L'async funziona meglio con il lavoro I/O-bound come richieste HTTP, query a database e operazioni su file, dove il codice aspetta sistemi esterni.
L'async non aiuta con il lavoro CPU-bound come calcoli complessi o elaborazione dati, dove il codice calcola attivamente invece di aspettare.
Il modo migliore per interiorizzare questi concetti è scrivere vero codice async. Nella prossima sezione, creerai la tua prima funzione async e vedrai esattamente come coroutine ed event loop lavorano insieme.
La tua prima funzione async in Python
Prima di scrivere codice async, guardiamo una normale funzione sincrona che aspetta prima di fare qualcosa:
import time
defgreet_after_delay():print("Starting...")
time.sleep(2)# Blocks for 2 secondsprint("Hello!")
greet_after_delay()Offerto da
La funzione funziona, ma time.sleep(2) blocca l'intero programma. In quei due secondi non può girare nient'altro.
Ecco la versione async:
import asyncio
asyncdefgreet_after_delay():print("Starting...")await asyncio.sleep(2)# Pauses, but doesn't blockprint("Hello!")
asyncio.run(greet_after_delay())Offerto da
L'output sembra identico, ma sotto il cofano succede qualcosa di diverso. Tre cambiamenti rendono questo codice async:
async def invece di def dichiara una coroutine.
await asyncio.sleep(2) invece di time.sleep(2) sospende senza bloccare.
asyncio.run() avvia l'event loop ed esegue la coroutine.
Nota che asyncio.sleep() è a sua volta una funzione async, per questo richiede await. Questa è una regola chiave: ogni funzione async deve essere chiamata con await. Che sia una builtin come asyncio.sleep() o una scritta da te, dimenticare await significa che non verrà effettivamente eseguita.
Al momento, la versione async non sembra più veloce. Questo perché abbiamo solo un task. Il vero vantaggio emerge quando esegui più coroutine insieme, come vedremo nella prossima sezione.
Un'altra cosa importante da sapere: non puoi chiamare una funzione async direttamente come una funzione normale. Proviamo:
result = greet_after_delay()print(result)print(type(result))Offerto da
<coroutine object greet_after_delay at 0x...>
<class 'coroutine'>Offerto da
Chiamando greet_after_delay() ottieni un oggetto coroutine, non il risultato. La funzione non viene effettivamente eseguita. Devi usare asyncio.run() oppure await per eseguirla dentro un'altra funzione.
Come funziona l'event loop
L'event loop è il motore dietro la programmazione async. Gestisce le coroutine e decide cosa eseguire e quando. Ecco cosa succede passo dopo passo quando esegui la funzione async greet_after_delay():
asyncio.run() crea un event loop.
L'event loop avvia greet_after_delay().
Viene stampato "Starting...".
Si incontra await asyncio.sleep(2) → la coroutine si mette in pausa.
L'event loop controlla: "Ci sono altri task da eseguire?" (al momento nessuno).
Passano 2 secondi, lo sleep termina.
L'event loop riprende greet_after_delay().
Viene stampato "Hello!".
La funzione finisce → l'event loop esce.
Il passo 5 è dove l'async diventa interessante. Con una sola coroutine, non c'è altro da fare. Ma quando hai più coroutine, l'event loop passa ad altro lavoro mentre una aspetta. Invece di restare inattivo durante due secondi di sleep, può eseguire altro codice.
Pensa all'event loop come a un vigile del traffico. Non rende più veloci le singole auto. Mantiene il traffico scorrevole lasciando passare altre auto mentre una è ferma.
Errore async comune: dimenticare await
Un errore frequente per chi inizia è dimenticare await quando si chiama una coroutine dentro un'altra funzione async:
Quando vedi un RuntimeWarning su una coroutine non in attesa, controlla di aver usato await su ogni chiamata a funzione async.
Task async concorrenti in Python
Nella sezione precedente abbiamo convertito una funzione sincrona in async. Ma non era più veloce. Questo perché abbiamo eseguito una sola coroutine. La vera potenza dell'async emerge quando esegui più coroutine contemporaneamente.
Perché await in sequenza resta comunque sequenziale
Potresti pensare che chiamare più funzioni async le faccia girare automaticamente in parallelo. Ma guarda cosa succede quando chiamiamo greet_after_delay() tre volte:
Starting Alice...
Hello, Alice!
Starting Bob...
Hello, Bob!
Starting Charlie...
Hello, Charlie!
Total time: 6.01 secondsOfferto da
Sei secondi per tre task da due secondi. Ogni await aspetta che la sua coroutine finisca prima di passare alla riga successiva. Il codice è async, ma sta girando in sequenza.
Esecuzione concorrente di task async con asyncio.gather()
Per eseguire coroutine contemporaneamente, usa asyncio.gather(). Accetta più coroutine e le esegue in concorrenza:
Starting Alice...
Starting Bob...
Starting Charlie...
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Total time: 2.00 secondsOfferto da
Due secondi invece di sei. Tutte e tre le coroutine sono partite subito, hanno dormito in parallelo e sono terminate insieme. È un'accelerazione di 3 volte con un solo cambiamento.
Nota l'ordine dell'output: tutti e tre i messaggi "Starting..." compaiono prima di qualsiasi "Hello...". Questo mostra che tutte le coroutine stanno girando nella stessa finestra di due secondi anziché aspettarsi a vicenda.
asyncio.gather() restituisce una lista di risultati nello stesso ordine in cui hai passato le coroutine. Se le tue coroutine restituiscono valori, puoi catturarli:
asyncdeffetch_number(n):await asyncio.sleep(1)return n *10asyncdefmain():
results =await asyncio.gather(
fetch_number(1),
fetch_number(2),
fetch_number(3),)print(results)
asyncio.run(main())Offerto da
I risultati tornano in ordine [10, 20, 30], corrispondente all'ordine delle coroutine passate a gather().
Richieste HTTP async in Python con aiohttp
Finora abbiamo usato asyncio.sleep() per simulare ritardi. Ora facciamo vere richieste HTTP. Potresti pensare alla libreria requests, ma qui non funziona. requests è sincrona e blocca l'event loop, vanificando lo scopo dell'async.
Usa invece aiohttp, un client HTTP asincrono costruito proprio per questo.
Introduzione ad aiohttp
Ecco come recuperare un URL con aiohttp:
import aiohttp
import asyncio
asyncdeffetch(url):asyncwith aiohttp.ClientSession()as session:asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
html =await fetch("https://example.com")print(f"Fetched {len(html)} characters")
asyncio.run(main())Offerto da
Nota i due blocchi annidati async with. Ognuno gestisce una risorsa diversa, e capirne il ruolo è fondamentale per usare correttamente aiohttp.
Come funziona ClientSession in aiohttp
Ecco cosa succede passo dopo passo quando fai richieste con aiohttp:
aiohttp.ClientSession() crea un connection pool (inizialmente vuoto).
session.get(url) controlla il pool: "C'è una connessione aperta a questo host (il server del sito)?"
Se non esiste una connessione, vengono creati una nuova connessione TCP (il protocollo di base per inviare dati su Internet) e un handshake SSL (la configurazione della cifratura per HTTPS).
Viene inviata una richiesta HTTP e si attende per gli header della risposta.
L'oggetto risposta trattiene la connessione.
await response.text() legge il body dai dati di rete.
Uscita dal blocco async with interno: la connessione torna al pool (resta aperta!).
La richiesta successiva allo stesso host viene effettuata riutilizzando la connessione del pool (salta il punto 3).
Uscita dal blocco async with esterno: tutte le connessioni nel pool vengono chiuse.
I passaggi 7 e 8 sono l'intuizione chiave. Il connection pool mantiene vive le connessioni tra una richiesta e l'altra. Quando fai un'altra richiesta allo stesso host, salti completamente gli handshake TCP e SSL.
Questo conta perché stabilire una nuova connessione è lento. Un handshake TCP richiede un round-trip al server. Un handshake SSL ne richiede altri due. A seconda della latenza, sono 100-300 ms prima ancora di inviare il primo byte di dati.
Usare una sessione condivisa per tutte le richieste
Ora capisci perché creare una nuova sessione per ogni richiesta è un problema:
# Wrong: new session for each requestasyncdeffetch_bad(url):asyncwith aiohttp.ClientSession()as session:asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
urls =["https://example.com"]*10
results =await asyncio.gather(*[fetch_bad(url)for url in urls])Offerto da
Ogni chiamata a fetch_bad() crea una nuova sessione con un pool vuoto. Ogni richiesta paga il costo completo degli handshake, anche se vanno tutte allo stesso host.
La soluzione è creare una sola sessione e passarla alla tua funzione di fetch:
# Right: reuse a single sessionasyncdeffetch_good(session, url):asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
urls =["https://example.com"]*10asyncwith aiohttp.ClientSession()as session:
results =await asyncio.gather(*[fetch_good(session, url)for url in urls])Offerto da
Con una sessione condivisa, la prima richiesta stabilisce la connessione e le restanti nove la riutilizzano. Un solo handshake invece di dieci.
Esempio di richiesta HTTP async: scraping di Hacker News
Mettiamo in pratica con la Hacker News API. Questa API è perfetta per dimostrare il comportamento asincrono perché recuperare le storie richiede più richieste. Se sei alle prime armi con le REST API in Python, consulta Python APIs: A Guide to Building and Using APIs per i concetti di base.
La struttura della Hacker News API:
https://hacker-news.firebaseio.com/v0/topstories.json restituisce una lista di ID di storie (solo numeri)
https://hacker-news.firebaseio.com/v0/item/{id}.json restituisce i dettagli di una singola storia
Per ottenere 10 storie, servono 11 richieste: una per la lista di ID, poi una per ogni storia. È esattamente qui che l'asincronia brilla.
Per prima cosa, vediamo cosa restituisce l'API se proviamo a recuperare la prima storia:
import aiohttp
import asyncio
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdefmain():asyncwith aiohttp.ClientSession()as session:# Get top story IDsasyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()print(f"Found {len(story_ids)} stories")print(f"First 5 IDs: {story_ids[:5]}")# Fetch first story details
first_id = story_ids[0]asyncwith session.get(f"{HN_API}/item/{first_id}.json")as response:
story =await response.json()print(f"\nStory structure:")for key, value in story.items():print(f" {key}: {repr(value)[:50]}")
asyncio.run(main())Offerto da
Found 500 stories
First 5 IDs: [46051449, 46055298, 46021577, 46053566, 45984864]
Story structure:
by: 'mikeayles'
descendants: 22
id: 46051449
kids: [46054027, 46053889, 46053275, 46053515, 46053002,
score: 217
text: 'I got DOOM running in KiCad by rendering it with
time: 1764108815
title: 'Show HN: KiDoom – Running DOOM on PCB Traces'
type: 'story'
url: 'https://www.mikeayles.com/#kidoom'Offerto da
L'API restituisce 500 ID di storie e ogni storia ha campi come title, url, score e by (l'autore).
Recuperare più risultati in sequenza vs. in concorrenza
Ora recuperiamo 10 storie in sequenza:
import aiohttp
import asyncio
import time
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
start = time.perf_counter()
stories =[]for story_id in story_ids[:10]:
story =await fetch_story(session, story_id)
stories.append(story)
elapsed = time.perf_counter()- start
print(f"Sequential: Fetched {len(stories)} stories in {elapsed:.2f} seconds")
asyncio.run(main())Offerto da
Sequential: Fetched 10 stories in 2.41 secondsOfferto da
Ora recuperiamo le stesse storie in concorrenza:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
start = time.perf_counter()
tasks =[fetch_story(session, story_id)for story_id in story_ids[:10]]
stories =await asyncio.gather(*tasks)
elapsed = time.perf_counter()- start
print(f"Concurrent: Fetched {len(stories)} stories in {elapsed:.2f} seconds")print("\nTop 3 stories:")for story in stories[:3]:print(f" - {story.get('title','No title')}")
asyncio.run(main())Offerto da
Concurrent: Fetched 10 stories in 0.69 seconds
Top 3 stories:
- Show HN: KiDoom – Running DOOM on PCB Traces
- AWS is 10x slower than a dedicated server for the same price [video]
- Surprisingly, Emacs on Android is pretty goodOfferto da
La versione concorrente è 3,5 volte più veloce. Invece di aspettare che ogni richiesta termini prima di avviare la successiva, tutte e 10 le richieste vengono eseguite contemporaneamente. È qui che la programmazione async ripaga davvero con l'I/O di rete reale.
Gestione errori e rate limiting in Python async
Quando recuperi dati in concorrenza, diverse cose possono andare storte. Potresti sovraccaricare il server con troppe richieste. Alcune richieste potrebbero bloccarsi per sempre. Altre potrebbero fallire subito. E quando ci sono errori, serve una strategia di recupero.
Questa sezione affronta ogni tema nell'ordine in cui si presenta: controllare quante richieste partono, impostare limiti di tempo, gestire i fallimenti e ritentare quando ha senso. Se ti serve un ripasso sui fondamenti della gestione eccezioni in Python, vedi Exception & Error Handling in Python. Useremo questa base per tutti gli esempi:
import aiohttp
import asyncio
import time
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()Offerto da
Rate limiting con semafori
Nella sezione precedente, abbiamo lanciato 10 richieste in una volta. Ha funzionato. Ma cosa succede quando devi recuperare 500 storie? O fare scraping di 10.000 pagine?
La maggior parte delle API applica limiti di velocità. Potrebbero consentire 10 richieste al secondo o 100 connessioni concorrenti. Se superi questi limiti, verrai bloccato, rallentato o bannato. Anche se l'API non impone limiti, lanciare migliaia di richieste simultaneamente può sovraccaricare il tuo sistema o il server.
Ti serve un modo per controllare quante richieste sono "in volo" in un dato momento. Questo è proprio ciò che fa un semaforo.
Un semaforo funziona come un sistema di permessi. Immagina di avere tre permessi. Qualsiasi task che vuole fare una richiesta deve prima ottenere un permesso. Quando finisce, lo restituisce, così una nuova richiesta può usarlo. Se non ci sono permessi disponibili, il task aspetta finché uno non si libera.
Ecco come va con 3 permessi e 4 o più task:
Ci sono tre permessi disponibili.
Il task A prende un permesso (ne restano 2) e avvia la richiesta.
Il task B prende un permesso (ne resta 1) e avvia la richiesta.
Il task C prende un permesso (ne restano 0) e avvia la richiesta.
Il task D vuole un permesso, ma non ce ne sono disponibili—aspetta.
Il task A termina e restituisce il suo permesso (1 disponibile).
Il task D prende quel permesso e avvia la richiesta.
Si continua così finché tutti i task non sono completati.
L'attesa al punto 5 è efficiente. Il task non gira in un loop a controllare "si è liberato un permesso?". Si sospende e lascia girare altro codice. L'event loop lo risveglia solo quando un permesso diventa disponibile.
Passiamo al codice. In asyncio, crei un semaforo con asyncio.Semaphore(n), dove n è il numero di permessi. Per usarlo, incapsula il codice in async with semaphore:. Questo acquisisce un permesso entrando nel blocco e lo rilascia automaticamente in uscita:
asyncdeffetch_story_limited(session, story_id, semaphore):asyncwith semaphore:# Acquire permit (or wait if none available)asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()# Permit automatically released hereOfferto da
Confrontiamo il recupero di 30 storie con e senza semaforo:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =(await response.json())[:30]# Without rate limiting: all 30 at once
start = time.perf_counter()await asyncio.gather(*[fetch_story(session, sid)for sid in story_ids])print(f"No limit: {time.perf_counter()- start:.2f}s (30 concurrent)")# With Semaphore(5): max 5 at a time
semaphore = asyncio.Semaphore(5)
start = time.perf_counter()await asyncio.gather(*[fetch_story_limited(session, sid, semaphore)for sid in story_ids])print(f"Semaphore(5): {time.perf_counter()- start:.2f}s (5 concurrent)")
asyncio.run(main())Offerto da
No limit: 0.62s (30 concurrent)
Semaphore(5): 1.50s (5 concurrent)Offerto da
La versione con semaforo è più lenta perché elabora le richieste in gruppi da cinque. Ma questo è il compromesso: sacrificare velocità per un comportamento prevedibile e rispettoso del server.
Nota: un semaforo limita le richieste concorrenti, non le richieste per unità di tempo. Semaphore(10) significa "al massimo 10 richieste in volo contemporaneamente", non "10 richieste al secondo". Se ti serve un rate limiting rigoroso basato sul tempo (ad esempio esattamente 10 richieste al secondo), puoi combinare un semaforo con ritardi tra i batch o usare una libreria come aiolimiter.
Timeout con asyncio.wait_for()
Anche con concorrenza controllata, singole richieste possono bloccarsi. Un server potrebbe accettare la connessione ma non rispondere mai. Senza un timeout, il tuo programma aspetta all'infinito.
La funzione asyncio.wait_for() incapsula qualsiasi coroutine con una scadenza. Le passi la coroutine e un timeout in secondi. Se l'operazione non termina in tempo, solleva asyncio.TimeoutError:
asyncdefslow_operation():print("Starting slow operation...")await asyncio.sleep(5)return"Done"asyncdefmain():try:
result =await asyncio.wait_for(slow_operation(), timeout=2.0)print(f"Success: {result}")except asyncio.TimeoutError:print("Operation timed out after 2 seconds")
asyncio.run(main())Offerto da
Starting slow operation...
Operation timed out after 2 secondsOfferto da
Quando il timeout scade, wait_for() annulla la coroutine. Puoi intercettare TimeoutError e decidere cosa fare: saltare la richiesta, restituire un valore di default o ritentare.
Per richieste concorrenti, incapsula ciascuna singolarmente. Ecco un helper che restituisce un dizionario di errore invece di sollevare un'eccezione:
Quando una coroutine viene annullata (per timeout o altre ragioni), Python solleva asyncio.CancelledError al suo interno. Se la tua coroutine detiene risorse come file handle o connessioni, usa try/finally per assicurarti che la pulizia avvenga anche in caso di cancellazione:
asyncdeffetch_with_cleanup(session, url):print("Starting fetch...")try:asyncwith session.get(url)as response:returnawait response.text()finally:print("Cleanup complete")# Runs even on cancellationOfferto da
Gestione errori con asyncio.gather()
I timeout intercettano le richieste lente. Ma alcune richieste falliscono subito con un errore. Vediamo cosa succede quando una richiesta in un batch fallisce.
Per prima cosa, ci serve una versione di fetch_story() che sollevi un'eccezione su ID non validi:
asyncdeffetch_story_strict(session, story_id):
story =await fetch_story(session, story_id)if story isNone:raise ValueError(f"Story not found: {story_id}")return storyOfferto da
Ora recuperiamo quattro storie valide più un ID non valido:
Con un ID non valido, perdiamo tutti e quattro i risultati riusciti. Per impostazione predefinita, gather() usa un comportamento fail-fast: una sola eccezione cancella tutto e viene propagata.
Per mantenere risultati parziali, aggiungi return_exceptions=True. Questo modifica il comportamento di gather(): invece di sollevare eccezioni, le restituisce come elementi nella lista dei risultati insieme ai valori riusciti:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
ids_to_fetch = story_ids[:4]+[99999999999]
results =await asyncio.gather(*[fetch_story_strict(session, sid)for sid in ids_to_fetch],
return_exceptions=True# Don't raise, return exceptions in list)# Separate successes from failures using isinstance()
stories =[r for r in results ifnotisinstance(r, Exception)]
errors =[r for r in results ifisinstance(r, Exception)]print(f"Got {len(stories)} stories, {len(errors)} failed")
asyncio.run(main())Offerto da
Il controllo isinstance(result, Exception) ti consente di separare i risultati riusciti dagli errori. Poi puoi elaborare ciò che ha funzionato e registrare o ritentare i fallimenti.
Logica di retry con backoff esponenziale
Alcuni errori sono temporanei. Un server potrebbe essere momentaneamente sovraccarico, o un intoppo di rete potrebbe interrompere la connessione. In questi casi, ha senso ritentare.
Ma ritentare subito può peggiorare le cose. Se un server è in difficoltà, martellarlo di retry aggrava il problema. Il backoff esponenziale lo risolve aspettando sempre più a lungo tra un tentativo e l'altro.
Lo schema usa 2 ** attempt per calcolare i tempi di attesa: il tentativo 0 aspetta un secondo (2⁰), il tentativo 1 due secondi (2¹), il tentativo 2 quattro secondi (2²) e così via. Questo dà al server sempre più tempo per riprendersi:
asyncdeffetch_with_retry(session, story_id, max_retries=3):for attempt inrange(max_retries):try:
story =await fetch_story(session, story_id)if story isNone:raise ValueError(f"Story {story_id} not found")return story
except(aiohttp.ClientError, ValueError):# Catch specific exceptionsif attempt == max_retries -1:print(f"Story {story_id}: Failed after {max_retries} attempts")returnNone
backoff =2** attempt # 1s, 2s, 4s...print(f"Story {story_id}: Attempt {attempt +1} failed, retrying in {backoff}s...")await asyncio.sleep(backoff)Offerto da
Nota che intercettiamo eccezioni specifiche (aiohttp.ClientError, ValueError) invece di un generico except. Così ritentiamo solo su errori che potrebbero essere transitori. Un KeyError dovuto a codice errato non dovrebbe attivare retry.
Testiamo con un mix di ID validi e non validi:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
test_ids =[story_ids[0],99999999999, story_ids[1],88888888888, story_ids[2]]
results =await asyncio.gather(*[fetch_with_retry(session, sid)for sid in test_ids])
successful =[r for r in results if r isnotNone]print(f"\nSuccessful: {len(successful)}, Failed: {len(test_ids)-len(successful)}")
asyncio.run(main())Offerto da
Story 99999999999: Attempt 1 failed, retrying in 1s...
Story 88888888888: Attempt 1 failed, retrying in 1s...
Story 99999999999: Attempt 2 failed, retrying in 2s...
Story 88888888888: Attempt 2 failed, retrying in 2s...
Story 99999999999: Failed after 3 attempts
Story 88888888888: Failed after 3 attempts
Successful: 3, Failed: 2Offerto da
In produzione, aggiungeresti anche jitter (piccoli ritardi casuali) per evitare che più richieste fallite ritentino esattamente nello stesso momento. Inoltre, ritenteresti solo errori transitori (problemi di rete lato server, come 503) rinunciando subito su quelli permanenti (ad esempio 404 o 401).
Archiviazione su database async in Python con aiosqlite
Abbiamo recuperato storie da Hacker News con rate limiting, timeout e gestione degli errori adeguati. Ora salviamole in un database.
Usare una libreria di database sincrona come sqlite3 bloccherebbe l'event loop durante le query, vanificando lo scopo della programmazione asincrona. Mentre il tuo codice aspetta il database, nessun'altra coroutine può girare. Per applicazioni async, ti serve una libreria di database async.
aiosqlite incapsula la sqlite3 integrata in Python in un'interfaccia async. Esegue le operazioni sul database in un thread pool così da non bloccare l'event loop. SQLite non richiede setup di server—è solo un file—quindi puoi eseguire subito questo codice. Se sei nuovo ai database in Python, il corso Introduction to Databases in Python copre le basi sincrone su cui aiosqlite costruisce.
Configurazione del database
Lo schema dovrebbe suonarti familiare. Proprio come aiohttp.ClientSession, usi async with per gestire la connessione:
import aiosqlite
asyncdefinit_db(db_path):asyncwith aiosqlite.connect(db_path)as db:await db.execute("""
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT,
score INTEGER,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")await db.commit()
asyncio.run(init_db("stories.db"))Offerto da
Le funzioni chiave:
aiosqlite.connect(path) apre (o crea) un file di database.
await db.execute(sql) esegue un'istruzione SQL.
await db.commit() salva le modifiche su disco.
Salvataggio delle storie
Ecco una funzione per salvare una singola storia:
asyncdefsave_story(db, story):await db.execute("INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",(story["id"], story.get("title",""), story.get("url",""), story.get("score",0)))Offerto da
I placeholder ? prevengono l'SQL injection—non usare mai f-string per inserire valori in SQL. INSERT OR REPLACE aggiorna le storie esistenti se le recuperiamo di nuovo.
Pipeline async completa in Python: fetch e store
Ora mettiamo insieme tutto quanto visto nel tutorial in una pipeline completa. Recupereremo 20 storie di Hacker News con rate limiting e le salveremo in un database:
import aiohttp
import aiosqlite
import asyncio
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()asyncdeffetch_story_limited(session, story_id, semaphore):asyncwith semaphore:
story =await fetch_story(session, story_id)if story:return story
returnNoneasyncdefsave_story(db, story):await db.execute("INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",(story["id"], story.get("title",""), story.get("url",""), story.get("score",0)))asyncdefmain():# Initialize databaseasyncwith aiosqlite.connect("hn_stories.db")as db:await db.execute("""
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT,
score INTEGER,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")# Fetch storiesasyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
semaphore = asyncio.Semaphore(5)
tasks =[fetch_story_limited(session, sid, semaphore)for sid in story_ids[:20]]
stories =await asyncio.gather(*tasks)# Save to databasefor story in stories:if story:await save_story(db, story)await db.commit()# Query and display
cursor =await db.execute("SELECT id, title, score FROM stories ORDER BY score DESC LIMIT 5")
rows =await cursor.fetchall()print(f"Saved {len([s for s in stories if s])} stories. Top 5 by score:")for row in rows:print(f" [{row[2]}] {row[1][:50]}")
asyncio.run(main())Offerto da
Saved 20 stories. Top 5 by score:
[671] Google Antigravity exfiltrates data via indirect p
[453] Trillions spent and big software projects are stil
[319] Ilya Sutskever: We're moving from the age of scali
[311] Show HN: We built an open source, zero webhooks pa
[306] FLUX.2: Frontier Visual IntelligenceOfferto da
La pipeline usa pattern di ogni sezione: ClientSession per il connection pooling, Semaphore(5) per il rate limiting, gather() per il fetch concorrente e ora aiosqlite per lo storage asincrono. Ogni componente gestisce la propria parte senza bloccare gli altri.
Ogni volta che esegui questo workflow, riceverai le top stories del giorno.
Conclusione
Questo tutorial ti ha portato dalla sintassi di base async/await a una pipeline dati completa. Hai imparato come le coroutine si sospendono e riprendono, come l'event loop gestisce i task concorrenti e come asyncio.gather() esegue più operazioni contemporaneamente. Hai aggiunto vere richieste HTTP con aiohttp, controllato la concorrenza con i semafori, gestito i fallimenti con timeout e retry e archiviato i risultati in un database con aiosqlite.
Usa l'async quando il tuo codice aspetta sistemi esterni: API HTTP, database, I/O su file o socket di rete. Per lavori pesanti sulla CPU come elaborazione dati o calcoli numerici, l'async non aiuta—valuta multiprocessing o concurrent.futures invece. Per approfondire, puoi esplorare la documentazione di asyncio e considerare FastAPI per creare web API asincrone.
Se vuoi costruire su queste conoscenze e imparare a progettare applicazioni intelligenti, dai un'occhiata al career track Associate AI Engineer for Developers.
FAQ su Python Async
Qual è la differenza tra programmazione async e sync in Python?⌄
Nella programmazione sincrona, il codice viene eseguito una riga alla volta e aspetta che ogni operazione termini. La programmazione async consente al tuo codice di avviare un'operazione e passare ad altro lavoro mentre aspetta, per poi riprendere quando il risultato è pronto. Il tutto è gestito da un event loop che passa da un task all'altro.
Quando dovrei usare la programmazione async invece del Python normale?⌄
Usa l'async per task I/O-bound in cui il tuo codice aspetta sistemi esterni: richieste HTTP, query a database, operazioni su file o socket di rete. L'async non aiuta con lavori CPU-bound come elaborazione dati o calcoli—per quelli usa multiprocessing o concurrent.futures.
Perché ricevo l'avviso "coroutine was never awaited"?⌄
Succede quando chiami una funzione async senza usare await. Chiamare una funzione async come get_data() restituisce un oggetto coroutine, non il risultato. Devi usare await get_data() per eseguirla davvero e ottenere il valore di ritorno.
Posso usare la libreria requests con asyncio?⌄
No, la libreria requests è sincrona e blocca l'event loop, vanificando lo scopo dell'async. Usa invece aiohttp—è un client HTTP async progettato per richieste concorrenti. Ricorda di riutilizzare una singola ClientSession per il connection pooling.
Come limito le richieste concorrenti per non sovraccaricare un'API?⌄
Usa asyncio.Semaphore per controllare quante richieste vengono eseguite simultaneamente. Crea un semaforo con il limite desiderato (ad esempio asyncio.Semaphore(5)) e incapsula ogni richiesta in async with semaphore. In questo modo solo quel numero di richieste sarà "in volo" contemporaneamente.
Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Scopri come integrare l'intelligenza artificiale nelle applicazioni software utilizzando API e librerie open-source. Inizia oggi il tuo percorso per diventare un ingegnere AI!
Crea API robuste e pronte per la produzione con FastAPI, padroneggiando operazioni HTTP, validazione ed esecuzione async per pipeline dati e ML efficienti.
Mentre il tuo script Python aspetta pazientemente le risposte delle API, le query al database o il completamento delle operazioni sui file, spesso quel tempo va sprecato. Con la programmazione asincrona in Python, il tuo codice può gestire più attività contemporaneamente. Così, mentre un'operazione è in attesa, le altre vanno avanti, trasformando i momenti morti in lavoro produttivo e riducendo spesso minuti di attesa a pochi secondi.
In questa guida ti insegnerò le basi della programmazione asincrona in Python lavorando su mini-progetti. Vedrai come coroutine, event loop e I/O asincrono possano rendere il tuo codice molto più reattivo.
Se vuoi imparare a creare web API asincrone, dai un'occhiata a questo corso su FastAPI.
Che cos'è la programmazione asincrona in Python?
Nel Python sincrono tradizionale, il codice viene eseguito una riga alla volta. Per esempio, quando chiami un'API, il programma si ferma e aspetta la risposta. Se ci vogliono due secondi, l'intero programma resta inattivo per due secondi. La programmazione asincrona consente al tuo codice di avviare una chiamata API e poi proseguire con altre attività.
Quando arriva la risposta, il codice riprende da dove si era fermato. Invece di aspettare che ogni operazione termini, puoi eseguirne più di una contemporaneamente. Questo è particolarmente importante quando il tuo codice passa tempo ad attendere sistemi esterni come database, API o file system.
Per far funzionare tutto, il sistema async di Python usa alcuni concetti chiave:
Coroutine: funzioni definite con
async definvece didef. Possono sospendere e riprendere l'esecuzione, rendendole perfette per operazioni che comportano attese.await: questa parola chiave dice a Python: "metti in pausa questa coroutine finché questa operazione non termina, ma lascia che nel frattempo giri altro codice".Event loop: il motore che gestisce tutte le coroutine, decidendo quale eseguire e quando passare da una all'altra.
Task: coroutine incapsulate per l'esecuzione concorrente. Le crei con
asyncio.create_task()per eseguire più operazioni in parallelo.Per evitare confusione su cosa la programmazione async può (e non può) fare, tieni a mente questo:
L'async funziona meglio con il lavoro I/O-bound come richieste HTTP, query a database e operazioni su file, dove il codice aspetta sistemi esterni.
L'async non aiuta con il lavoro CPU-bound come calcoli complessi o elaborazione dati, dove il codice calcola attivamente invece di aspettare.
Il modo migliore per interiorizzare questi concetti è scrivere vero codice async. Nella prossima sezione, creerai la tua prima funzione async e vedrai esattamente come coroutine ed event loop lavorano insieme.
La tua prima funzione async in Python
Prima di scrivere codice async, guardiamo una normale funzione sincrona che aspetta prima di fare qualcosa:
La funzione funziona, ma
time.sleep(2)blocca l'intero programma. In quei due secondi non può girare nient'altro.Ecco la versione async:
L'output sembra identico, ma sotto il cofano succede qualcosa di diverso. Tre cambiamenti rendono questo codice async:
async definvece didefdichiara una coroutine.await asyncio.sleep(2)invece ditime.sleep(2)sospende senza bloccare.asyncio.run()avvia l'event loop ed esegue la coroutine.Nota che
asyncio.sleep()è a sua volta una funzione async, per questo richiedeawait. Questa è una regola chiave: ogni funzione async deve essere chiamata conawait. Che sia una builtin comeasyncio.sleep()o una scritta da te, dimenticareawaitsignifica che non verrà effettivamente eseguita.Al momento, la versione async non sembra più veloce. Questo perché abbiamo solo un task. Il vero vantaggio emerge quando esegui più coroutine insieme, come vedremo nella prossima sezione.
Un'altra cosa importante da sapere: non puoi chiamare una funzione async direttamente come una funzione normale. Proviamo:
Chiamando
greet_after_delay()ottieni un oggetto coroutine, non il risultato. La funzione non viene effettivamente eseguita. Devi usareasyncio.run()oppureawaitper eseguirla dentro un'altra funzione.Come funziona l'event loop
L'event loop è il motore dietro la programmazione async. Gestisce le coroutine e decide cosa eseguire e quando. Ecco cosa succede passo dopo passo quando esegui la funzione async
greet_after_delay():asyncio.run()crea un event loop.L'event loop avvia
greet_after_delay().Viene stampato "Starting...".
Si incontra
await asyncio.sleep(2)→ la coroutine si mette in pausa.L'event loop controlla: "Ci sono altri task da eseguire?" (al momento nessuno).
Passano 2 secondi, lo sleep termina.
L'event loop riprende
greet_after_delay().Viene stampato "Hello!".
La funzione finisce → l'event loop esce.
Il passo 5 è dove l'async diventa interessante. Con una sola coroutine, non c'è altro da fare. Ma quando hai più coroutine, l'event loop passa ad altro lavoro mentre una aspetta. Invece di restare inattivo durante due secondi di sleep, può eseguire altro codice.
Pensa all'event loop come a un vigile del traffico. Non rende più veloci le singole auto. Mantiene il traffico scorrevole lasciando passare altre auto mentre una è ferma.
Errore async comune: dimenticare await
Un errore frequente per chi inizia è dimenticare
awaitquando si chiama una coroutine dentro un'altra funzione async:Senza
await, ottieni l'oggetto coroutine invece del valore di ritorno. Python ti avvisa anche che la coroutine non è mai stata eseguita.La correzione è semplice:
Quando vedi un
RuntimeWarningsu una coroutine non in attesa, controlla di aver usatoawaitsu ogni chiamata a funzione async.Task async concorrenti in Python
Nella sezione precedente abbiamo convertito una funzione sincrona in async. Ma non era più veloce. Questo perché abbiamo eseguito una sola coroutine. La vera potenza dell'async emerge quando esegui più coroutine contemporaneamente.
Perché await in sequenza resta comunque sequenziale
Potresti pensare che chiamare più funzioni async le faccia girare automaticamente in parallelo. Ma guarda cosa succede quando chiamiamo
greet_after_delay()tre volte:Sei secondi per tre task da due secondi. Ogni
awaitaspetta che la sua coroutine finisca prima di passare alla riga successiva. Il codice è async, ma sta girando in sequenza.Esecuzione concorrente di task async con asyncio.gather()
Per eseguire coroutine contemporaneamente, usa
asyncio.gather(). Accetta più coroutine e le esegue in concorrenza:Due secondi invece di sei. Tutte e tre le coroutine sono partite subito, hanno dormito in parallelo e sono terminate insieme. È un'accelerazione di 3 volte con un solo cambiamento.
Nota l'ordine dell'output: tutti e tre i messaggi "Starting..." compaiono prima di qualsiasi "Hello...". Questo mostra che tutte le coroutine stanno girando nella stessa finestra di due secondi anziché aspettarsi a vicenda.
asyncio.gather()restituisce una lista di risultati nello stesso ordine in cui hai passato le coroutine. Se le tue coroutine restituiscono valori, puoi catturarli:I risultati tornano in ordine
[10, 20, 30], corrispondente all'ordine delle coroutine passate agather().Richieste HTTP async in Python con aiohttp
Finora abbiamo usato
asyncio.sleep()per simulare ritardi. Ora facciamo vere richieste HTTP. Potresti pensare alla libreriarequests, ma qui non funziona.requestsè sincrona e blocca l'event loop, vanificando lo scopo dell'async.Usa invece
aiohttp, un client HTTP asincrono costruito proprio per questo.Introduzione ad aiohttp
Ecco come recuperare un URL con
aiohttp:Nota i due blocchi annidati
async with. Ognuno gestisce una risorsa diversa, e capirne il ruolo è fondamentale per usare correttamenteaiohttp.Come funziona ClientSession in aiohttp
Ecco cosa succede passo dopo passo quando fai richieste con
aiohttp:aiohttp.ClientSession()crea un connection pool (inizialmente vuoto).session.get(url)controlla il pool: "C'è una connessione aperta a questo host (il server del sito)?"Se non esiste una connessione, vengono creati una nuova connessione TCP (il protocollo di base per inviare dati su Internet) e un handshake SSL (la configurazione della cifratura per HTTPS).
Viene inviata una richiesta HTTP e si attende per gli header della risposta.
L'oggetto risposta trattiene la connessione.
await response.text()legge il body dai dati di rete.Uscita dal blocco
async withinterno: la connessione torna al pool (resta aperta!).La richiesta successiva allo stesso host viene effettuata riutilizzando la connessione del pool (salta il punto 3).
Uscita dal blocco
async withesterno: tutte le connessioni nel pool vengono chiuse.I passaggi 7 e 8 sono l'intuizione chiave. Il connection pool mantiene vive le connessioni tra una richiesta e l'altra. Quando fai un'altra richiesta allo stesso host, salti completamente gli handshake TCP e SSL.
Questo conta perché stabilire una nuova connessione è lento. Un handshake TCP richiede un round-trip al server. Un handshake SSL ne richiede altri due. A seconda della latenza, sono 100-300 ms prima ancora di inviare il primo byte di dati.
Usare una sessione condivisa per tutte le richieste
Ora capisci perché creare una nuova sessione per ogni richiesta è un problema:
Ogni chiamata a
fetch_bad()crea una nuova sessione con un pool vuoto. Ogni richiesta paga il costo completo degli handshake, anche se vanno tutte allo stesso host.La soluzione è creare una sola sessione e passarla alla tua funzione di fetch:
Con una sessione condivisa, la prima richiesta stabilisce la connessione e le restanti nove la riutilizzano. Un solo handshake invece di dieci.
Esempio di richiesta HTTP async: scraping di Hacker News
Mettiamo in pratica con la Hacker News API. Questa API è perfetta per dimostrare il comportamento asincrono perché recuperare le storie richiede più richieste. Se sei alle prime armi con le REST API in Python, consulta Python APIs: A Guide to Building and Using APIs per i concetti di base.
La struttura della Hacker News API:
https://hacker-news.firebaseio.com/v0/topstories.jsonrestituisce una lista di ID di storie (solo numeri)https://hacker-news.firebaseio.com/v0/item/{id}.jsonrestituisce i dettagli di una singola storiaPer ottenere 10 storie, servono 11 richieste: una per la lista di ID, poi una per ogni storia. È esattamente qui che l'asincronia brilla.
Per prima cosa, vediamo cosa restituisce l'API se proviamo a recuperare la prima storia:
L'API restituisce 500 ID di storie e ogni storia ha campi come
title,url,scoreeby(l'autore).Recuperare più risultati in sequenza vs. in concorrenza
Ora recuperiamo 10 storie in sequenza:
Ora recuperiamo le stesse storie in concorrenza:
La versione concorrente è 3,5 volte più veloce. Invece di aspettare che ogni richiesta termini prima di avviare la successiva, tutte e 10 le richieste vengono eseguite contemporaneamente. È qui che la programmazione async ripaga davvero con l'I/O di rete reale.
Gestione errori e rate limiting in Python async
Quando recuperi dati in concorrenza, diverse cose possono andare storte. Potresti sovraccaricare il server con troppe richieste. Alcune richieste potrebbero bloccarsi per sempre. Altre potrebbero fallire subito. E quando ci sono errori, serve una strategia di recupero.
Questa sezione affronta ogni tema nell'ordine in cui si presenta: controllare quante richieste partono, impostare limiti di tempo, gestire i fallimenti e ritentare quando ha senso. Se ti serve un ripasso sui fondamenti della gestione eccezioni in Python, vedi Exception & Error Handling in Python. Useremo questa base per tutti gli esempi:
Rate limiting con semafori
Nella sezione precedente, abbiamo lanciato 10 richieste in una volta. Ha funzionato. Ma cosa succede quando devi recuperare 500 storie? O fare scraping di 10.000 pagine?
La maggior parte delle API applica limiti di velocità. Potrebbero consentire 10 richieste al secondo o 100 connessioni concorrenti. Se superi questi limiti, verrai bloccato, rallentato o bannato. Anche se l'API non impone limiti, lanciare migliaia di richieste simultaneamente può sovraccaricare il tuo sistema o il server.
Ti serve un modo per controllare quante richieste sono "in volo" in un dato momento. Questo è proprio ciò che fa un semaforo.
Un semaforo funziona come un sistema di permessi. Immagina di avere tre permessi. Qualsiasi task che vuole fare una richiesta deve prima ottenere un permesso. Quando finisce, lo restituisce, così una nuova richiesta può usarlo. Se non ci sono permessi disponibili, il task aspetta finché uno non si libera.
Ecco come va con 3 permessi e 4 o più task:
Ci sono tre permessi disponibili.
Il task A prende un permesso (ne restano 2) e avvia la richiesta.
Il task B prende un permesso (ne resta 1) e avvia la richiesta.
Il task C prende un permesso (ne restano 0) e avvia la richiesta.
Il task D vuole un permesso, ma non ce ne sono disponibili—aspetta.
Il task A termina e restituisce il suo permesso (1 disponibile).
Il task D prende quel permesso e avvia la richiesta.
Si continua così finché tutti i task non sono completati.
L'attesa al punto 5 è efficiente. Il task non gira in un loop a controllare "si è liberato un permesso?". Si sospende e lascia girare altro codice. L'event loop lo risveglia solo quando un permesso diventa disponibile.
Passiamo al codice. In asyncio, crei un semaforo con
asyncio.Semaphore(n), dovenè il numero di permessi. Per usarlo, incapsula il codice inasync with semaphore:. Questo acquisisce un permesso entrando nel blocco e lo rilascia automaticamente in uscita:Confrontiamo il recupero di 30 storie con e senza semaforo:
La versione con semaforo è più lenta perché elabora le richieste in gruppi da cinque. Ma questo è il compromesso: sacrificare velocità per un comportamento prevedibile e rispettoso del server.
Nota: un semaforo limita le richieste concorrenti, non le richieste per unità di tempo.
Semaphore(10)significa "al massimo 10 richieste in volo contemporaneamente", non "10 richieste al secondo". Se ti serve un rate limiting rigoroso basato sul tempo (ad esempio esattamente 10 richieste al secondo), puoi combinare un semaforo con ritardi tra i batch o usare una libreria comeaiolimiter.Timeout con asyncio.wait_for()
Anche con concorrenza controllata, singole richieste possono bloccarsi. Un server potrebbe accettare la connessione ma non rispondere mai. Senza un timeout, il tuo programma aspetta all'infinito.
La funzione
asyncio.wait_for()incapsula qualsiasi coroutine con una scadenza. Le passi la coroutine e untimeoutin secondi. Se l'operazione non termina in tempo, sollevaasyncio.TimeoutError:Quando il timeout scade,
wait_for()annulla la coroutine. Puoi intercettareTimeoutErrore decidere cosa fare: saltare la richiesta, restituire un valore di default o ritentare.Per richieste concorrenti, incapsula ciascuna singolarmente. Ecco un helper che restituisce un dizionario di errore invece di sollevare un'eccezione:
Quando una coroutine viene annullata (per timeout o altre ragioni), Python solleva
asyncio.CancelledErroral suo interno. Se la tua coroutine detiene risorse come file handle o connessioni, usatry/finallyper assicurarti che la pulizia avvenga anche in caso di cancellazione:Gestione errori con asyncio.gather()
I timeout intercettano le richieste lente. Ma alcune richieste falliscono subito con un errore. Vediamo cosa succede quando una richiesta in un batch fallisce.
Per prima cosa, ci serve una versione di
fetch_story()che sollevi un'eccezione su ID non validi:Ora recuperiamo quattro storie valide più un ID non valido:
Con un ID non valido, perdiamo tutti e quattro i risultati riusciti. Per impostazione predefinita,
gather()usa un comportamento fail-fast: una sola eccezione cancella tutto e viene propagata.Per mantenere risultati parziali, aggiungi
return_exceptions=True. Questo modifica il comportamento digather(): invece di sollevare eccezioni, le restituisce come elementi nella lista dei risultati insieme ai valori riusciti:Il controllo
isinstance(result, Exception)ti consente di separare i risultati riusciti dagli errori. Poi puoi elaborare ciò che ha funzionato e registrare o ritentare i fallimenti.Logica di retry con backoff esponenziale
Alcuni errori sono temporanei. Un server potrebbe essere momentaneamente sovraccarico, o un intoppo di rete potrebbe interrompere la connessione. In questi casi, ha senso ritentare.
Ma ritentare subito può peggiorare le cose. Se un server è in difficoltà, martellarlo di retry aggrava il problema. Il backoff esponenziale lo risolve aspettando sempre più a lungo tra un tentativo e l'altro.
Lo schema usa
2 ** attemptper calcolare i tempi di attesa: il tentativo 0 aspetta un secondo (2⁰), il tentativo 1 due secondi (2¹), il tentativo 2 quattro secondi (2²) e così via. Questo dà al server sempre più tempo per riprendersi:Nota che intercettiamo eccezioni specifiche (
aiohttp.ClientError,ValueError) invece di un genericoexcept. Così ritentiamo solo su errori che potrebbero essere transitori. UnKeyErrordovuto a codice errato non dovrebbe attivare retry.Testiamo con un mix di ID validi e non validi:
In produzione, aggiungeresti anche jitter (piccoli ritardi casuali) per evitare che più richieste fallite ritentino esattamente nello stesso momento. Inoltre, ritenteresti solo errori transitori (problemi di rete lato server, come 503) rinunciando subito su quelli permanenti (ad esempio 404 o 401).
Archiviazione su database async in Python con aiosqlite
Abbiamo recuperato storie da Hacker News con rate limiting, timeout e gestione degli errori adeguati. Ora salviamole in un database.
Usare una libreria di database sincrona come
sqlite3bloccherebbe l'event loop durante le query, vanificando lo scopo della programmazione asincrona. Mentre il tuo codice aspetta il database, nessun'altra coroutine può girare. Per applicazioni async, ti serve una libreria di database async.aiosqliteincapsula lasqlite3integrata in Python in un'interfaccia async. Esegue le operazioni sul database in un thread pool così da non bloccare l'event loop. SQLite non richiede setup di server—è solo un file—quindi puoi eseguire subito questo codice. Se sei nuovo ai database in Python, il corso Introduction to Databases in Python copre le basi sincrone su cuiaiosqlitecostruisce.Configurazione del database
Lo schema dovrebbe suonarti familiare. Proprio come
aiohttp.ClientSession, usiasync withper gestire la connessione:Le funzioni chiave:
aiosqlite.connect(path)apre (o crea) un file di database.await db.execute(sql)esegue un'istruzione SQL.await db.commit()salva le modifiche su disco.Salvataggio delle storie
Ecco una funzione per salvare una singola storia:
I placeholder
?prevengono l'SQL injection—non usare mai f-string per inserire valori in SQL.INSERT OR REPLACEaggiorna le storie esistenti se le recuperiamo di nuovo.Pipeline async completa in Python: fetch e store
Ora mettiamo insieme tutto quanto visto nel tutorial in una pipeline completa. Recupereremo 20 storie di Hacker News con rate limiting e le salveremo in un database:
La pipeline usa pattern di ogni sezione:
ClientSessionper il connection pooling,Semaphore(5)per il rate limiting,gather()per il fetch concorrente e oraaiosqliteper lo storage asincrono. Ogni componente gestisce la propria parte senza bloccare gli altri.Ogni volta che esegui questo workflow, riceverai le top stories del giorno.
Conclusione
Questo tutorial ti ha portato dalla sintassi di base
async/awaita una pipeline dati completa. Hai imparato come le coroutine si sospendono e riprendono, come l'event loop gestisce i task concorrenti e comeasyncio.gather()esegue più operazioni contemporaneamente. Hai aggiunto vere richieste HTTP conaiohttp, controllato la concorrenza con i semafori, gestito i fallimenti con timeout e retry e archiviato i risultati in un database conaiosqlite.Usa l'async quando il tuo codice aspetta sistemi esterni: API HTTP, database, I/O su file o socket di rete. Per lavori pesanti sulla CPU come elaborazione dati o calcoli numerici, l'async non aiuta—valuta
multiprocessingoconcurrent.futuresinvece. Per approfondire, puoi esplorare la documentazione di asyncio e considerare FastAPI per creare web API asincrone.Se vuoi costruire su queste conoscenze e imparare a progettare applicazioni intelligenti, dai un'occhiata al career track Associate AI Engineer for Developers.