Python-Pakete und das Import-System
So funktionieren Python-Pakete: Paket mit __init__.py erstellen, absolute und relative Imports verwenden, eine saubere API definieren und Fallstricke vermeiden.
Ein Paket ist ein Verzeichnis mit Python-Modulen, das als eine einzige importierbare Einheit behandelt wird. Während ein Modul aus einer einzigen .py-Datei besteht, ist ein Paket ein Ordner — der potenziell viele Module und Unterpakete enthält — durch den das Import-System von Python wie durch einen Baum navigieren kann. Dieses Kapitel erklärt, wie man Pakete erstellt, steuert, was sie nach außen freigeben, absolute und relative Imports korrekt schreibt und die Fallstricke vermeidet, über die Einsteiger stolpern.
Module vs. Pakete — der wesentliche Unterschied
Ein Modul ist eine einzelne .py-Datei:
greetings.py ← moduleEin Paket ist ein Verzeichnis, das mindestens eine spezielle Datei namens __init__.py enthält:
greetings/ ← package
__init__.py
english.py
spanish.pyBeide werden mit dem gleichen import-Schlüsselwort importiert, aber ein Paket bietet eine Namespace-Hierarchie: greetings.english und greetings.spanish sind separate Module, teilen sich jedoch den greetings-Namespace.
Wann ein Modul, wann ein Paket verwenden:
| Situation | Verwende |
|---|---|
| Ein kleines, in sich geschlossenes Hilfsprogramm | Modul (eine .py-Datei) |
| Mehrere verwandte Module unter einem gemeinsamen Namen | Paket (ein Verzeichnis) |
| Eine Bibliothek, die auf PyPI veröffentlicht werden soll | Paket (mit src/-Layout) |
Die Datei __init__.py
__init__.py ist das, was ein Verzeichnis zu einem Paket macht. Python führt sie beim ersten Import des Pakets (oder eines seiner Module) aus. Sie kann leer sein oder:
- Namen aus Untermodulen importieren, um sie auf Paketebene verfügbar zu machen
- Paketweite Initialisierung durchführen (Logging-Konfiguration, Versions-Checks usw.)
__all__definieren, umfrom package import *zu steuern
Minimales Paket-Layout
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}!"Import von außerhalb des Pakets:
from myapp.utils import greet
print(greet("Alice")) # Hello, Alice!Namen auf Paketebene bereitstellen
Ein gängiges Muster besteht darin, die am häufigsten verwendeten Namen in __init__.py zu importieren, damit Aufrufer from myapp import greet schreiben können statt from myapp.utils import greet.
# myapp/__init__.py
from .utils import greet
from .config import MAX_RETRIESNun sind beide Namen direkt am Paket verfügbar:
import myapp
print(myapp.greet("Bob")) # Hello, Bob!
print(myapp.MAX_RETRIES) # whatever config.py definesAbsolute Imports
Ein absoluter Import beginnt immer beim Top-Level-Paket oder einem Verzeichnis in sys.path. Er hängt nie davon ab, wo sich die importierende Datei befindet.
project/
myapp/
__init__.py
utils.py
services/
__init__.py
email.pyIn email.py sieht ein absoluter Import so aus:
# 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}")Absolute Imports sind der Standard und der empfohlene Stil (PEP 8). Sie sind eindeutig, unabhängig davon, wie Python aufgerufen wird.
Relative Imports
Ein relativer Import verwendet Punkte (.), um relativ zum Speicherort der aktuellen Datei im Paketbaum zu navigieren.
.bezeichnet das aktuelle Paket..bezeichnet das übergeordnete Paket...bezeichnet das überübergeordnete Paket usw.
# 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 greetWann relative Imports verwenden
Relative Imports sind innerhalb eines Pakets nützlich, wenn man deutlich machen möchte, dass greet aus diesem Paket stammt und nicht aus einer externen Bibliothek gleichen Namens. Sie erleichtern auch das Refactoring, da die Imports mit dem Paket mitziehen, wenn man das Top-Level-Verzeichnis umbenennt.
Achtung: Relative Imports funktionieren nur innerhalb eines Pakets. Wenn man python myapp/utils.py direkt ausführt, behandelt Python die Datei als eigenständiges Skript und nicht als Teil eines Pakets, und ein relativer Import löst ImportError: attempted relative import with no known parent package aus. Verwende stattdessen 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.utilsDie öffentliche API mit __all__ steuern
__all__ ist eine Liste von Namen, die from package import * exportiert. Sie dokumentiert auch, was das Paket als öffentlich betrachtet.
# myapp/__init__.py
from .utils import greet, farewell
from .config import MAX_RETRIES
__all__ = ["greet", "MAX_RETRIES"] # farewell is intentionally not exportedNun importiert from myapp import * nur greet und MAX_RETRIES. Die farewell-Funktion existiert weiterhin, ist aber nicht Teil der beworbenen öffentlichen Schnittstelle. Namen mit einem führenden Unterstrich (_private) werden per Konvention ebenfalls von import * ausgeschlossen.
Verschachtelte Pakete (Unterpakete)
Pakete können andere Pakete enthalten. Jedes Unterverzeichnis benötigt eine eigene __init__.py.
analytics/
__init__.py
reports/
__init__.py
daily.py
weekly.py
charts/
__init__.py
bar.py
pie.pyImportiere ein tief verschachteltes Modul mit dem vollständigen gepunkteten Pfad:
from analytics.reports.daily import generate_report
from analytics.charts.bar import BarChartOder, wenn analytics/__init__.py sie bereitstellt:
# analytics/__init__.py
from .reports.daily import generate_report# caller
from analytics import generate_reportWie tief sollte man gehen?
Ein drei oder vier Ebenen tiefes Paket ist meist ein Zeichen dafür, dass es zu groß geworden ist und in separate Top-Level-Pakete aufgeteilt werden sollte (die unabhängig installiert werden können). Für die meisten Projekte sind zwei Ebenen (package.module) ausreichend.
Ein vollständiges Beispiel: Ein geometry-Paket aufbauen
Wir bauen Schritt für Schritt ein kleines, aber realistisches Paket.
Verzeichnis-Layout
geometry/
__init__.py
shapes.py
conversions.pyshapes.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 * heightconversions.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 — die wichtigsten Namen freilegen
# 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",
]Das Paket verwenden
# 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.5707963267948966Oder mit selektiven Imports:
from geometry import circle_area, degrees_to_radians
print(circle_area(3)) # 28.274333882308138
print(degrees_to_radians(180)) # 3.141592653589793Namespace-Pakete (Python 3.3+)
Seit Python 3.3 ist ein Verzeichnis ohne __init__.py ein Namespace-Paket. Python führt alle Verzeichnisse gleichen Namens in sys.path zu einem logischen Paket zusammen. Dies ist hauptsächlich für große Organisationen nützlich, die ein einzelnes Paket auf mehrere Repositories oder Installationsverzeichnisse aufteilen.
Für die tägliche Entwicklung empfiehlt es sich, immer __init__.py einzufügen. Das macht die Absicht eindeutig und funktioniert mit allen Python-Versionen.
Wie Python Pakete findet
Wenn man import geometry schreibt, durchsucht Python sys.path der Reihe nach:
- Das Verzeichnis des laufenden Skripts (oder das aktuelle Verzeichnis im interaktiven Modus)
- Verzeichnisse in der Umgebungsvariable
PYTHONPATH - Die Standardbibliotheks-Verzeichnisse
- Das
site-packages-Verzeichnis (wo mit pip installierte Pakete liegen)
import sys
print(sys.path)Das Paketverzeichnis muss sich direkt in einem dieser Orte befinden. Wenn geometry/ in /home/alice/projects/ liegt, findet Python es nicht, sofern /home/alice/projects/ nicht in sys.path eingetragen ist.
Tipp: Verwende eine virtuelle Umgebung und installiere dein Paket im Entwicklungsmodus (pip install -e .), damit Python es immer findet, ohne dass eine manuelle sys.path-Manipulation nötig ist.
Ein Paket verteilen
Um ein Paket mit anderen zu teilen (oder es mit pip zu installieren), benötigst du eine pyproject.toml im Projektstamm:
my_project/
pyproject.toml ← build metadata
src/
geometry/
__init__.py
shapes.py
conversions.pyEine minimale pyproject.toml:
[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"Lokal im editierbaren Modus während der Entwicklung installieren:
pip install -e .Nun funktioniert import geometry überall in der virtuellen Umgebung, unabhängig vom aktuellen Verzeichnis.
Häufige Fallstricke
Fehlende __init__.py
Wenn man __init__.py vergisst, behandelt Python 3 das Verzeichnis als Namespace-Paket (was meistens noch funktioniert), aber Python 2 ignoriert es vollständig. Sei explizit: füge immer __init__.py hinzu.
Ein Paket genauso benennen wie ein stdlib-Modul
Vermeide Namen wie math/, json/, os/, email/. Python könnte dein Paket anstelle des Standard-Bibliotheks-Moduls importieren und so unzusammenhängenden Code beschädigen.
Ein Paket-Modul als Skript ausführen
Wie oben beschrieben, bricht das direkte Ausführen von python myapp/services/email.py relative Imports. Verwende stattdessen python -m myapp.services.email.
Zirkuläre Imports zwischen Modulen desselben Pakets
Wenn shapes.py aus conversions.py importiert und conversions.py aus shapes.py importiert, liegt ein zirkulärer Import vor. Symptome sind ImportError oder Namen, die unerwartet als None erscheinen. Die Lösung besteht meistens darin, die gemeinsame Logik in ein drittes Modul auszulagern oder den Import in einen Funktionsrumpf zu verlagern.
# 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 bei relativen Imports außerhalb eines Pakets
# 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.pyFühre es als python -m myapp.utils aus oder strukturiere es so um, dass der Einstiegspunkt ein separates Skript ist, das das Paket importiert.
Zusammenfassung
| Konzept | Kurzbeschreibung |
|---|---|
| Paket | Ein Verzeichnis mit __init__.py, das Module enthält |
__init__.py | Macht ein Verzeichnis zu einem Paket; wird beim ersten Import ausgeführt |
| Absoluter Import | from myapp.utils import greet — immer von der Wurzel |
| Relativer Import | from ..utils import greet — relativ zur aktuellen Datei |
__all__ | Listet Namen auf, die from package import * exportiert |
| Namespace-Paket | Verzeichnis ohne __init__.py; nur Python 3.3+ |
| Editierbares Install | pip install -e . — Paket überall in der venv auffindbar |
Siehe Python-Module für die Codeorganisation in einzelnen Dateien, Python pip zum Installieren von Drittanbieter-Paketen und Python Virtual Environments zum isolierten Verwalten von Projektabhängigkeiten.