Memory Leak Testing con Python e Pytest: Guida Completa 2025

Memory Leak Testing in Python
Previeni i memory leak con il tuo test suite
Mauto 5 min

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:

  1. Prima di ogni test, misuriamo l’utilizzo della memoria
  2. Eseguiamo il test
  3. Dopo il test, misuriamo di nuovo la memoria
  4. 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

  1. Usa Context Manager: Il pattern with in Python è tuo amico

    with open("file.txt") as f:
        contenuto = f.read()
    # Il file viene chiuso automaticamente
    
  2. 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
    
  3. 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.

content_copy Copiato