Python: gestire import circolari tra moduli e pacchetti in modo pulito

by theArchitect
SHARE
Python: gestire import circolari tra moduli e pacchetti in modo pulito
© Guida-HTML5.it

Introduzione

Quando un progetto Python cresce, i moduli iniziano a dipendere gli uni dagli altri. In questo scenario può comparire un problema molto comune e spesso sottovalutato: l’import circolare. Succede quando due o più moduli si importano a vicenda, direttamente o indirettamente, creando una catena che Python non riesce a risolvere completamente durante il caricamento.

Questo tutorial ti mostra come riconoscere e risolvere gli import circolari in modo pratico, usando un esempio realistico con un piccolo pacchetto. È un argomento molto utile perché migliora la struttura del progetto, riduce gli errori difficili da diagnosticare e ti aiuta a scrivere codice più manutenibile.

Vedremo un caso tipico, poi una soluzione concreta con una migliore organizzazione delle responsabilità tra moduli. L’obiettivo non è solo “far funzionare il codice”, ma costruire una base architetturale più solida.

Codice completo

Immaginiamo un piccolo pacchetto chiamato shop, con tre moduli:

  • models.py: contiene le classi dati
  • services.py: contiene la logica di business
  • main.py: avvia il programma

La versione iniziale contiene un import circolare. Poi vedremo la versione corretta.

# shop/models.py
from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float


@dataclass
class OrderItem:
    product: Product
    quantity: int

    def total(self) -
# shop/services.py
from shop.models import Product, OrderItem

def create_sample_order() -> OrderItem:
    # In questo esempio il servizio crea un ordine di test
    product = Product(name="Tastiera meccanica", price=89.90)
    return OrderItem(product=product, quantity=2)
# shop/main.py
from shop.services import create_sample_order

def main() -> None:
    order_item = create_sample_order()
    print(f"Prodotto: {order_item.product.name}")
    print(f"Totale: {order_item.total():.2f} €")

if __name__ == "__main__":
    main()

Fin qui tutto bene. Ma ora supponiamo che, per comodità, il modulo models.py abbia bisogno di una funzione di supporto definita in services.py. Per esempio, un metodo che formatti la descrizione dell’ordine.

# shop/models.py
from dataclasses import dataclass
from shop.services import format_order_description

@dataclass
class Product:
    name: str
    price: float


@dataclass
class OrderItem:
    product: Product
    quantity: int

    def total(self) -> float:
        return self.product.price * self.quantity

    def description(self) -> str:
        return format_order_description(self)
# shop/services.py
from shop.models import Product, OrderItem

def format_order_description(order_item: OrderItem) -> str:
    return f"{order_item.quantity} x {order_item.product.name}"

def create_sample_order() -> OrderItem:
    product = Product(name="Tastiera meccanica", price=89.90)
    return OrderItem(product=product, quantity=2)

Questa struttura genera un import circolare: models.py importa services.py e services.py importa models.py. Python prova a inizializzare i moduli, ma uno dei due non è ancora completamente caricato quando serve all’altro.

La soluzione migliore è separare le responsabilità: i modelli devono contenere dati e comportamenti strettamente legati ai dati, mentre la logica di presentazione o formattazione deve stare in un modulo esterno, oppure in un modulo di utility indipendente.

Ecco una versione corretta.

# shop/models.py
from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float


@dataclass
class OrderItem:
    product: Product
    quantity: int

    def total(self) -> float:
        return self.product.price * self.quantity
# shop/utils.py
from shop.models import OrderItem

def format_order_description(order_item: OrderItem) -> str:
    return f"{order_item.quantity} x {order_item.product.name}"
# shop/services.py
from shop.models import Product, OrderItem
from shop.utils import format_order_description

def create_sample_order() -> OrderItem:
    product = Product(name="Tastiera meccanica", price=89.90)
    return OrderItem(product=product, quantity=2)

def print_order_summary(order_item: OrderItem) -> None:
    print(format_order_description(order_item))
    print(f"Totale: {order_item.total():.2f} €")
# shop/main.py
from shop.services import create_sample_order, print_order_summary

def main() -> None:
    order_item = create_sample_order()
    print_order_summary(order_item)

if __name__ == "__main__":
    main()

Spiegazione

L’errore di import circolare nasce spesso quando si mescolano troppo presto concetti diversi nello stesso modulo. In Python, l’import avviene durante l’esecuzione del file, non solo come dichiarazione statica. Questo significa che se un modulo A importa B, e B importa A, uno dei due potrebbe essere ancora “a metà” del caricamento.

Nel nostro esempio iniziale, models.py voleva usare una funzione di services.py. Ma il modulo dei servizi, a sua volta, dipendeva dai modelli. Il risultato è una dipendenza bidirezionale difficile da mantenere.

La soluzione adottata è semplice ma molto efficace:

  • models.py contiene solo strutture dati e metodi legati ai dati
  • services.py gestisce la logica applicativa
  • utils.py ospita funzioni indipendenti e riutilizzabili

Questa separazione riduce il rischio di import circolari perché ogni modulo ha una responsabilità chiara. Inoltre, rende il codice più testabile: puoi testare i modelli senza avviare i servizi e viceversa.

Un altro vantaggio è la leggibilità. Quando apri un modulo, capisci subito cosa aspettarti: dati, logica o utility. Nei progetti reali, questa disciplina evita che i file diventino “contenitori misti” difficili da estendere.

Nota anche che abbiamo usato from shop.models import OrderItem in utils.py. Questo è sicuro perché utils.py non viene importato da models.py. La direzione delle dipendenze è importante: idealmente, le dipendenze devono andare da moduli ad alto livello verso moduli più semplici, non il contrario.

Best practice

  • Evita di importare logica applicativa nei modelli: i modelli dovrebbero restare leggeri e focalizzati sui dati.
  • Separa le utility pure: funzioni di formattazione, calcolo o conversione possono stare in moduli indipendenti.
  • Non usare import “per comodità” senza valutare l’impatto sulla struttura del progetto.
  • Preferisci dipendenze unidirezionali: modelli base, poi servizi, poi interfaccia o script di avvio.
  • Usa import locali solo quando necessario: in alcuni casi un import dentro una funzione può rompere un ciclo, ma è spesso una soluzione temporanea, non architetturale.
  • Raggruppa il codice per responsabilità: se un modulo cresce troppo, è probabile che stia facendo troppe cose.
  • Testa i moduli in isolamento: se un file non può essere importato da solo, forse ha troppe dipendenze.

Un trucco utile è chiedersi: “Questo pezzo di codice appartiene davvero a questo modulo?” Se la risposta è no, probabilmente è il momento di estrarlo in un file dedicato.

Riepilogo

Gli import circolari sono un problema frequente nei progetti Python organizzati in moduli e pacchetti. Si verificano quando due moduli dipendono l’uno dall’altro in modo diretto o indiretto, causando errori di caricamento o comportamenti inattesi.

La strategia più efficace non è aggirare il problema con soluzioni improvvisate, ma ripensare la struttura del pacchetto:

  • mantieni i modelli focalizzati sui dati
  • sposta la logica di business nei servizi
  • estrai le funzioni comuni in moduli di utility
  • mantieni le dipendenze il più possibile unidirezionali

Con questa organizzazione, il progetto diventa più stabile, più chiaro e più facile da evolvere nel tempo.

Approfondisci con risorse ufficiali

  • Python Documentation - The import system
  • Python Documentation - Modules
  • Python Documentation - Packages
  • Python Documentation - dataclasses
  • PEP 8 - Style Guide for Python Code

Se vuoi approfondire davvero la gestione dei moduli in Python, il passo successivo è studiare come progettare dipendenze pulite tra pacchetti, soprattutto in applicazioni medio-grandi. È lì che la qualità dell’architettura fa la differenza.

SHARE