Memory Leak Testing con Python e Pytest: Guida Completa 2025

Hai presente quando il tuo programma inizia a mangiare memoria come un teenager affamato davanti al frigo aperto a mezzanotte? Ecco, probabilmente stai affrontando un memory leak. Non preoccuparti, capita anche nelle migliori famiglie (di codice). Oggi ti spiegherò come dare la caccia a questi fastidiosi bug utilizzando il tuo test suite, perché a volte il soluzione è più vicina di quanto pensi.
Quando la Memoria Diventa un Problema
Immagina di essere al ristorante. Ordini un piatto dopo l’altro, ma nessuno viene mai a portare via i piatti vuoti. Prima o poi finirai lo spazio sul tavolo, proprio come il tuo programma finirà la memoria disponibile se continua ad allocarne senza mai liberarla.
Ho scoperto quanto può essere subdolo questo problema proprio la settimana scorsa, quando il mio script Python è morto silenziosamente dopo ore di esecuzione. Il sistema operativo l’ha gentilmente “accompagnato alla porta” (leggi: brutalmente terminato) con un messaggio nei log del kernel che diceva più o meno: “Mi spiace amico, ma 42GB di RAM sono un po’ troppi anche per me”.
Come Funziona un Memory Leak in Python?
Python è un linguaggio con garbage collection automatico, il che significa che in teoria dovrebbe occuparsi lui di pulire la memoria non più utilizzata. È come avere un maggiordomo che sistema la tua stanza. Ma proprio come un maggiordomo non può buttare via qualcosa che pensi ancora di utilizzare, Python non può liberare memoria se mantieni riferimenti agli oggetti.
Ecco un esempio classico di memory leak in Python:
from functools import cache
@cache
def calcola_sequenza(n: int) -> list[int]:
return list(range(n))
# Questo sembra innocuo, ma...
for i in range(1_000_000):
result = calcola_sequenza(i)
# Fai qualcosa con result
Sembra innocuo vero? Eppure questo codice sta silenziosamente riempiendo la memoria del tuo computer. Il decoratore @cache
memorizza ogni risultato della funzione, quindi stai effettivamente conservando in memoria un milione di liste diverse.
Come Catturare i Memory Leak con il Test Suite
La buona notizia è che puoi utilizzare il tuo test suite per identificare questi problemi prima che si manifestino in produzione. È come avere un metal detector per i bug della memoria.
Ecco come funziona il processo:
- Prima di ogni test, misuriamo l’utilizzo della memoria
- Eseguiamo il test
- Dopo il test, misuriamo di nuovo la memoria
- Se c’è una differenza significativa, abbiamo trovato un potenziale leak
Implementazione con Pytest
Ecco come implementare questa soluzione utilizzando pytest. Creiamo un file conftest.py
nella directory dei test:
import gc
import os
import tracemalloc
import pytest
if os.getenv("CHECK_LEAKS") == "1":
@pytest.fixture(autouse=True)
def check_for_memory_leaks():
# Avvia il tracciamento della memoria
tracemalloc.start()
# Pulisce la memoria non utilizzata
gc.collect()
# Misura l'utilizzo attuale
memoria_iniziale = tracemalloc.get_traced_memory()[0]
try:
# Esegue il test
yield
finally:
# Pulizia dopo il test
gc.collect()
memoria_finale = tracemalloc.get_traced_memory()[0]
# Consideriamo un leak solo se superiore a 10KB
assert (
memoria_finale - memoria_iniziale < 10_000
), "Houston, abbiamo un memory leak!"
tracemalloc.stop()
Questo codice è come un detective che controlla le tue tasche prima e dopo che entri in un negozio. Se esci con più “roba” di quando sei entrato, qualcosa non quadra.
Come Usarlo nei Tuoi Test
Una volta implementato il fixture, puoi eseguire i tuoi test normalmente con l’aggiunta di una variabile d’ambiente:
env CHECK_LEAKS=1 pytest
Se hai un memory leak, vedrai qualcosa del genere:
======= test session starts =======
collected 9 items
test_api.py .....F.... [100%]
======= FAILURES =======
_ _ _ _ _ _ _ _ _ _ _ _
AssertionError: Houston, abbiamo un memory leak!
Debugging dei Memory Leak
Quando trovi un memory leak, il primo passo è identificare quale parte del codice lo sta causando. Nel nostro esempio precedente con la funzione calcola_sequenza
, il problema è abbastanza evidente: stiamo cachando troppi risultati senza limite.
Una soluzione potrebbe essere utilizzare @lru_cache
invece di @cache
:
from functools import lru_cache
@lru_cache(maxsize=1000) # Mantiene solo gli ultimi 1000 risultati
def calcola_sequenza(n: int) -> list[int]:
return list(range(n))
Altri Tipi di Resource Leak
I memory leak non sono gli unici tipi di leak che possono affliggere il tuo programma. Ci sono anche:
- File descriptor leak: quando apri file o socket e non li chiudi
- Database connection leak: connessioni al database che rimangono aperte
- GPU memory leak: memoria della scheda grafica non liberata
- Disk space leak: spazio su disco che cresce indefinitamente
La buona notizia è che puoi utilizzare lo stesso approccio basato sui test per identificare questi problemi. Ad esempio, per i file descriptor:
import psutil
import pytest
@pytest.fixture(autouse=True)
def check_file_descriptors():
process = psutil.Process()
fds_iniziali = process.num_fds()
yield
fds_finali = process.num_fds()
assert fds_finali <= fds_iniziali, "File descriptor leak!"
Best Practices per Prevenire i Memory Leak
Usa Context Manager: Il pattern
with
in Python è tuo amicowith open("file.txt") as f: contenuto = f.read() # Il file viene chiuso automaticamente
Attenzione alle Closure: Le funzioni annidate possono mantenere riferimenti non voluti
def factory(): data = [1, 2, 3] # Questa lista rimarrà in memoria def inner(): return data return inner
Monitora l’Uso delle Cache: Usa limiti appropriati per le cache in memoria
@lru_cache(maxsize=100) # Meglio di @cache illimitato def funzione_costosa(x): return x ** 2
Conclusione: La Memoria è Importante
I memory leak sono come le perdite d’acqua in una tubatura: all’inizio potresti non accorgertene, ma alla fine del mese la bolletta ti farà piangere. Utilizzare il tuo test suite per identificarli preventivamente è un approccio pratico ed efficace.
Il vantaggio di questo metodo è che si integra naturalmente nel tuo workflow di sviluppo. Non hai bisogno di strumenti specializzati o di sessioni di debug separate - è tutto parte del tuo normale processo di testing.
Ricorda sempre che la memoria del tuo computer non è infinita (anche se con 128GB di RAM potrebbe sembrarlo). Un po’ di attenzione preventiva può salvarti da molti mal di testa futuri.
P.S. Se questo articolo ti è stato utile, la prossima volta che il tuo programma crasherà per un out-of-memory, almeno saprai da dove iniziare a cercare!
Nota: Gli esempi di codice in questo articolo sono stati testati con Python 3.10+ e pytest 7.0+. Se stai usando versioni precedenti, potresti dover adattare leggermente il codice.