W3docs

Pacchetti Python e il sistema di importazione

Scopri come funzionano i pacchetti Python: crea un pacchetto con __init__.py, usa importazioni assolute e relative, esponi un'API pubblica e evita gli errori comuni.

Un pacchetto è una directory di moduli Python che si tratta come un'unica unità importabile. Mentre un modulo è un singolo file .py, un pacchetto è una cartella — che può contenere molti moduli e sotto-pacchetti — che il sistema di importazione di Python può navigare come un albero. Questo capitolo spiega come creare pacchetti, controllare ciò che espongono, scrivere correttamente importazioni assolute e relative ed evitare le insidie che mettono in difficoltà i principianti.

Moduli vs. Pacchetti — la differenza fondamentale

Un modulo è un singolo file .py:

greetings.py        ← module

Un pacchetto è una directory che contiene almeno un file speciale chiamato __init__.py:

greetings/          ← package
    __init__.py
    english.py
    spanish.py

Entrambi vengono importati con la stessa parola chiave import, ma un pacchetto fornisce una gerarchia di spazi dei nomi: greetings.english e greetings.spanish sono moduli separati, eppure condividono lo spazio dei nomi greetings.

Quando usare un modulo vs. un pacchetto:

SituazioneUsa
Un'utilità piccola e autonomaModulo (un singolo file .py)
Più moduli correlati da raggruppare sotto un unico nomePacchetto (una directory)
Una libreria da distribuire su PyPIPacchetto (con layout src/)

Il file __init__.py

__init__.py è ciò che rende una directory un pacchetto. Python lo esegue la prima volta che il pacchetto (o uno qualsiasi dei suoi moduli) viene importato. Può essere vuoto, oppure può:

  • Importare nomi dai sotto-moduli per renderli disponibili a livello di pacchetto
  • Eseguire l'inizializzazione a livello di pacchetto (configurazione del logging, controlli di versione, ecc.)
  • Definire __all__ per controllare from package import *

Layout minimo del pacchetto

myapp/
    __init__.py       ← can be empty
    utils.py
    config.py
# myapp/__init__.py  (empty — that is fine)
# myapp/utils.py
def greet(name):
    return f"Hello, {name}!"

Importazione dall'esterno del pacchetto:

from myapp.utils import greet

print(greet("Alice"))   # Hello, Alice!

Rendere i nomi disponibili a livello di pacchetto

Un pattern comune è importare i nomi più utilizzati in __init__.py in modo che i chiamanti possano scrivere from myapp import greet invece di from myapp.utils import greet.

# myapp/__init__.py
from .utils import greet
from .config import MAX_RETRIES

Ora entrambi i nomi sono disponibili direttamente nel pacchetto:

import myapp

print(myapp.greet("Bob"))   # Hello, Bob!
print(myapp.MAX_RETRIES)    # whatever config.py defines

Importazioni assolute

Un'importazione assoluta parte sempre dal pacchetto di livello superiore o da una directory presente in sys.path. Non dipende mai dalla posizione del file che effettua l'importazione.

project/
    myapp/
        __init__.py
        utils.py
        services/
            __init__.py
            email.py

All'interno di email.py, un'importazione assoluta si presenta così:

# myapp/services/email.py
from myapp.utils import greet   # absolute — starts from the top-level package

def send_welcome(user):
    message = greet(user)
    print(f"Sending: {message}")

Le importazioni assolute sono lo stile predefinito e raccomandato (PEP 8). Sono non ambigue indipendentemente da come viene invocato Python.

Importazioni relative

Un'importazione relativa usa i punti (.) per navigare nell'albero dei pacchetti relativamente alla posizione del file corrente.

  • . indica il pacchetto corrente
  • .. indica il pacchetto padre
  • ... indica il pacchetto nonno, e così via
# myapp/services/email.py

# One dot — import from myapp.services (same directory)
from . import sms

# Two dots — import from myapp (parent directory)
from ..utils import greet

Quando usare le importazioni relative

Le importazioni relative sono utili all'interno di un pacchetto quando si vuole rendere chiaro che greet proviene da questo pacchetto e non da una libreria esterna con lo stesso nome. Facilitano anche il refactoring perché le importazioni si spostano insieme al pacchetto se si rinomina la directory di livello superiore.

Attenzione: le importazioni relative funzionano solo all'interno di un pacchetto. Se esegui python myapp/utils.py direttamente, Python lo tratta come uno script autonomo, non come parte di un pacchetto, e un'importazione relativa genera ImportError: attempted relative import with no known parent package. Esegui invece il pacchetto con python -m myapp.utils.

# Wrong — runs utils.py as a script, breaking relative imports
$ python myapp/utils.py

# Right — runs utils.py as part of the myapp package
$ python -m myapp.utils

Controllare l'API pubblica con __all__

__all__ è un elenco di nomi che from package import * esporta. Documenta anche ciò che il pacchetto considera pubblico.

# myapp/__init__.py
from .utils import greet, farewell
from .config import MAX_RETRIES

__all__ = ["greet", "MAX_RETRIES"]   # farewell is intentionally not exported

Ora from myapp import * importa solo greet e MAX_RETRIES. La funzione farewell esiste ancora; semplicemente non fa parte dell'interfaccia pubblica dichiarata. I nomi con prefisso underscore singolo (_private) sono esclusi da import * per convenzione.

Pacchetti annidati (sotto-pacchetti)

I pacchetti possono contenere altri pacchetti. Ogni sotto-directory necessita del proprio __init__.py.

analytics/
    __init__.py
    reports/
        __init__.py
        daily.py
        weekly.py
    charts/
        __init__.py
        bar.py
        pie.py

Importa un modulo profondamente annidato con il percorso completo con punti:

from analytics.reports.daily import generate_report
from analytics.charts.bar import BarChart

Oppure, se analytics/__init__.py li espone:

# analytics/__init__.py
from .reports.daily import generate_report
# caller
from analytics import generate_report

Quanto in profondità conviene scendere?

Un pacchetto con tre o quattro livelli di profondità è generalmente un segnale che è cresciuto troppo e dovrebbe essere suddiviso in pacchetti di livello superiore separati (installabili separatamente). Per la maggior parte dei progetti, due livelli (pacchetto.modulo) sono sufficienti.

Esempio pratico: costruire un pacchetto geometry

Costruiamo passo dopo passo un pacchetto piccolo ma realistico.

Layout della directory

geometry/
    __init__.py
    shapes.py
    conversions.py

shapes.py

# geometry/shapes.py
import math

def circle_area(radius):
    """Return the area of a circle with the given radius."""
    if radius < 0:
        raise ValueError("radius must be non-negative")
    return math.pi * radius ** 2

def rectangle_area(width, height):
    """Return the area of a rectangle."""
    return width * height

def triangle_area(base, height):
    """Return the area of a triangle."""
    return 0.5 * base * height

conversions.py

# geometry/conversions.py

def degrees_to_radians(degrees):
    """Convert degrees to radians."""
    import math
    return degrees * math.pi / 180

def radians_to_degrees(radians):
    """Convert radians to degrees."""
    import math
    return radians * 180 / math.pi

__init__.py — esporre i nomi chiave

# geometry/__init__.py
"""
geometry — simple 2-D geometry utilities.

Public API:
    circle_area(radius) -> float
    rectangle_area(width, height) -> float
    triangle_area(base, height) -> float
    degrees_to_radians(degrees) -> float
    radians_to_degrees(radians) -> float
"""

from .shapes import circle_area, rectangle_area, triangle_area
from .conversions import degrees_to_radians, radians_to_degrees

__all__ = [
    "circle_area",
    "rectangle_area",
    "triangle_area",
    "degrees_to_radians",
    "radians_to_degrees",
]

Usare il pacchetto

# main.py (sits next to the geometry/ directory)
import geometry

print(geometry.circle_area(5))           # 78.53981633974483
print(geometry.rectangle_area(4, 6))     # 24
print(geometry.degrees_to_radians(90))   # 1.5707963267948966

Oppure con importazioni selettive:

from geometry import circle_area, degrees_to_radians

print(circle_area(3))              # 28.274333882308138
print(degrees_to_radians(180))     # 3.141592653589793

Pacchetti di spazio dei nomi (Python 3.3+)

Da Python 3.3, una directory senza __init__.py è un namespace package. Python unisce in un unico pacchetto logico tutte le directory con lo stesso nome presenti in sys.path. Questo è principalmente utile per grandi organizzazioni che suddividono un singolo pacchetto tra più repository o directory di installazione.

Per lo sviluppo quotidiano, includi sempre __init__.py. Rende le tue intenzioni non ambigue e funziona con tutte le versioni di Python.

Come Python trova i pacchetti

Quando scrivi import geometry, Python ricerca in sys.path nell'ordine:

  1. La directory dello script in esecuzione (o la directory corrente in modalità interattiva)
  2. Le directory nella variabile d'ambiente PYTHONPATH
  3. Le directory della libreria standard
  4. La directory site-packages (dove risiedono i pacchetti installati con pip)
import sys
print(sys.path)

La directory del pacchetto deve trovarsi direttamente all'interno di una di queste posizioni. Se geometry/ si trova in /home/alice/projects/, Python non la troverà a meno che /home/alice/projects/ non sia presente in sys.path.

Suggerimento: usa un ambiente virtuale e installa il tuo pacchetto in modalità sviluppo (pip install -e .) in modo che Python lo trovi sempre senza manipolazioni manuali di sys.path.

Distribuire un pacchetto

Per condividere un pacchetto con altri (o installarlo con pip), hai bisogno di un file pyproject.toml nella radice del progetto:

my_project/
    pyproject.toml    ← build metadata
    src/
        geometry/
            __init__.py
            shapes.py
            conversions.py

Un pyproject.toml minimale:

[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "geometry"
version = "0.1.0"
description = "Simple 2-D geometry utilities"
requires-python = ">=3.9"

Installa localmente in modalità modificabile durante lo sviluppo:

pip install -e .

Ora import geometry funziona ovunque nel tuo ambiente virtuale, indipendentemente dalla directory corrente.

Errori comuni

__init__.py mancante

Se dimentichi __init__.py, Python 3 tratta la directory come un namespace package (il che di solito funziona ancora), ma Python 2 la ignora completamente. Sii esplicito: aggiungi sempre __init__.py.

Nominare un pacchetto come un modulo della libreria standard

Evita nomi come math/, json/, os/, email/. Python potrebbe importare il tuo pacchetto al posto di quello della libreria standard, rompendo codice non correlato.

Eseguire un modulo di pacchetto come script

Come notato in precedenza, eseguire python myapp/services/email.py direttamente rompe le importazioni relative. Usa invece python -m myapp.services.email.

Importazioni circolari tra moduli dello stesso pacchetto

Se shapes.py importa da conversions.py e conversions.py importa da shapes.py, si crea un'importazione circolare. I sintomi includono ImportError o nomi che appaiono inaspettatamente come None. La soluzione è solitamente spostare la logica condivisa in un terzo modulo, oppure ritardare l'importazione all'interno del corpo di una funzione.

# Delayed import — breaks the cycle at module load time
def some_function():
    from .shapes import circle_area   # imported only when the function is called
    ...

ImportError quando si usano importazioni relative fuori da un pacchetto

# Will raise: ImportError: attempted relative import with no known parent package
# if run as:  python myapp/utils.py

from . import config   # relative import inside utils.py

Eseguilo come python -m myapp.utils oppure ristruttura in modo che il punto di ingresso sia uno script separato che importa il pacchetto.

ConcettoDescrizione sintetica
PacchettoUna directory con __init__.py contenente moduli
__init__.pyRende una directory un pacchetto; viene eseguito alla prima importazione
Importazione assolutafrom myapp.utils import greet — sempre dalla radice
Importazione relativafrom ..utils import greet — relativa al file corrente
__all__Elenca i nomi esportati da from package import *
Namespace packageDirectory senza __init__.py; solo Python 3.3+
Installazione modificabilepip install -e . — pacchetto trovato ovunque nel venv

Vedi Python Modules per l'organizzazione del codice in un singolo file, Python pip per installare pacchetti di terze parti e Python Virtual Environments per mantenere isolate le dipendenze del progetto.

Pratica

Pratica
Cosa rende una directory un pacchetto Python (nelle versioni di Python precedenti alla 3.3)?
Cosa rende una directory un pacchetto Python (nelle versioni di Python precedenti alla 3.3)?
Was this page helpful?