Virtual environment, pip und pyproject.toml in Python

1. Übersicht

Wenn du an mehreren Python-Projekten auf derselben Maschine arbeitest, wirst du bald auf Versionskonflikte stoßen. Projekt A benötigt requests 2.28, Projekt B dagegen requests 2.32. Dieser Artikel führt drei Werkzeuge ein, die das Problem lösen. venv isoliert die Umgebung für jedes Projekt. pip installiert und verwaltet Pakete. pyproject.toml deklariert das Projekt nach modernem Standard. Nach dem Lesen wirst du in der Lage sein, ein strukturiertes Python-Projekt zu erstellen, es im editierbaren Modus zu installieren und Tests mit pytest auszuführen.

2. Virtuelle Umgebung

Eine virtuelle Umgebung (venv) ist ein Verzeichnis, das drei Dinge enthält: einen separaten Python-Interpreter (normalerweise ein Symlink), ein eigenes Verzeichnis site-packages und Aktivierungsskripte. Pakete, die du in der venv installierst, befinden sich nur in diesem Verzeichnis und beeinflussen nicht das Python der Systeminstallation.

2.1. venv erstellen und aktivieren

Das Modul venv ist seit Python 3.3 in der Standardbibliothek enthalten, du brauchst also nichts zusätzlich zu installieren:

mkdir shopcart && cd shopcart
python3 -m venv .venv

# Linux / macOS
source .venv/bin/activate

# Windows (PowerShell)
.venv\Scripts\Activate.ps1

Der Befehl python3 -m venv .venv erstellt das Verzeichnis .venv im Projekt. Den Namen .venv zu verwenden ist eine weit verbreitete Konvention, und die meisten IDEs erkennen ihn automatisch. Nach der Aktivierung hat der Shell-Prompt das Präfix (.venv). Die Befehle python und pip zeigen jetzt auf die venv.

Um die venv zu beenden, führe aus:

deactivate

2.2. Überprüfe, welchen Interpreter du nutzt

Das ist ein Schritt, den viele übersehen, und er ist die Quelle vieler Fehler wie “ich habe es installiert, aber ich bekomme immer noch ModuleNotFoundError”:

which python
python -c "import sys; print(sys.prefix); print(sys.prefix != sys.base_prefix)"
/home/lan/shopcart/.venv/bin/python
/home/lan/shopcart/.venv
True

sys.prefix ist das Wurzelverzeichnis der laufenden Umgebung. sys.base_prefix ist das Verzeichnis der ursprünglichen Python-Installation. Unterschiedliche Werte bedeuten, dass du in einer venv bist.

Hinweis: Commit nicht das Verzeichnis .venv zu Git. Füge .venv/ zu .gitignore hinzu. Die venv hängt von absoluten Pfaden und dem Betriebssystem ab, daher wird sie beim Kopieren auf eine andere Maschine normalerweise beschädigt. Die richtige Vorgehensweise ist, die Abhängigkeitsdatei zu committen und dann die venv auf der neuen Maschine neu zu erstellen.

3. Pakete mit pip verwalten

pip ist das Standard-Installationsprogramm für Pakete und bezieht diese von PyPI.

3.1. Grundlegende Befehle

python -m pip install --upgrade pip
python -m pip install "requests>=2.31,<3"
python -m pip list
python -m pip show requests
python -m pip uninstall -y requests

Die Befehle bewirken folgendes:

  • install --upgrade pip aktualisiert pip selbst. Eine ältere Version von pip kann pyproject.toml manchmal nicht korrekt lesen.
  • "requests>=2.31,<3" ist ein version specifier. Du musst ihn in Anführungszeichen setzen, damit die Shell das < nicht als Umleitung interpretiert.
  • list listet installierte Pakete auf. show druckt detaillierte Informationen aus, einschließlich Version, Installationsort und Abhängigkeiten.

3.2. Warum solltest du python -m pip statt pip verwenden?

Der Befehl pip kann auf ein anderes Python verweisen als der Befehl python, das du gerade nutzt. Das passiert häufig, wenn die Maschine mehrere Python-Versionen hat oder wenn du vergessen hast, die venv zu aktivieren. Der Befehl python -m pip führt immer pip des korrekten Interpreters aus, daher werden Pakete sicher in die Umgebung installiert, die du möchtest.

3.3. requirements.txt und pip freeze

Die traditionelle Methode zum Austausch einer Paketliste ist die Verwendung von requirements.txt:

python -m pip freeze > requirements.txt
python -m pip install -r requirements.txt
certifi==2024.8.30
charset-normalizer==3.4.0
idna==3.10
requests==2.32.3
urllib3==2.2.3

pip freeze speichert alle Pakete in der venv mit genauen Versionen, einschließlich indirekter Abhängigkeiten wie urllib3. Diese Methode eignet sich, um eine Laufzeitumgebung zu reproduzieren. Allerdings wird nicht klar, welche Pakete du wirklich brauchst und welche nur als Abhängigkeiten mitgezogen werden. Sie beschreibt auch nicht dein Projekt: Name, Version, Python-Anforderungen. Das ist Aufgabe der pyproject.toml.

4. Projekt mit pyproject.toml deklarieren

pyproject.toml ist die Standard-Konfigurationsdatei für ein Python-Projekt. Sie ist in den Standards PEP 518, PEP 621 und verwandten PEPs definiert. Diese Datei ersetzt setup.py, setup.cfg und fasst die Konfiguration für viele Tools wie pytest oder ruff an einem Ort zusammen.

4.1. Projektstruktur

Wir verwenden das src layout, d.h. wir platzieren Code in src/. Diese Struktur hilft sicherzustellen, dass Tests immer auf das installierte Paket laufen, nicht versehentlich direkt vom Arbeitsverzeichnis importieren:

shopcart/
├── pyproject.toml
├── src/
│   └── shopcart/
│       ├── __init__.py
│       ├── order.py
│       └── cli.py
└── tests/
    └── test_order.py

4.2. Komplette pyproject.toml Datei

[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"

[project]
name = "shopcart"
version = "0.1.0"
description = "Einfache Berechnung des Bestelltotals"
requires-python = ">=3.11"
dependencies = [
    "requests>=2.31,<3",
]

[project.optional-dependencies]
dev = [
    "pytest>=8",
    "ruff>=0.5",
]

[project.scripts]
shopcart = "shopcart.cli:main"

[tool.pytest.ini_options]
testpaths = ["tests"]

Die Bedeutung jedes Abschnitts:

  • [build-system] teilt pip mit, welche Tools zum Erstellen des Projekts benötigt werden. Hier ist es setuptools. Du kannst es durch hatchling oder flit_core ersetzen, ohne den Rest zu ändern.
  • [project] enthält Standard-Metadaten. requires-python verhindert, dass jemand das Projekt auf einer nicht unterstützten Python-Version installiert.
  • dependencies sind die Pakete, die erforderlich sind, um das Projekt auszuführen. Hier solltest du Versionsbereiche angeben (>=2.31,<3) und nicht an genaue Versionen gebunden sein (==2.32.3).
  • [project.optional-dependencies] gruppiert optionale Pakete. Die dev Gruppe enthält Entwicklungswerkzeuge, die Endbenutzer nicht brauchen.
  • [project.scripts] erstellt einen shopcart Befehl im Terminal. Dieser Befehl ruft die main Funktion im Modul shopcart.cli auf.
  • [tool.pytest.ini_options] ist die spezifische Konfiguration für pytest. Jedes Tool hat seinen eigenen Namespace [tool.<name>].

Mit src layout findet setuptools automatisch das Paket in src/, du brauchst also keine zusätzliche Deklaration.

4.3. Code des Pakets

# src/shopcart/order.py
from dataclasses import dataclass, field
from decimal import Decimal


@dataclass(frozen=True)
class OrderLine:
    product: str
    unit_price: Decimal
    quantity: int


@dataclass
class Order:
    customer: str
    lines: list[OrderLine] = field(default_factory=list)

    def add(self, product: str, unit_price: str, quantity: int = 1) -> None:
        if quantity <= 0:
            raise ValueError("quantity muss größer als 0 sein")
        self.lines.append(OrderLine(product, Decimal(unit_price), quantity))

    def total(self) -> Decimal:
        return sum((l.unit_price * l.quantity for l in self.lines), Decimal("0"))

Order speichert eine Liste von OrderLine. Für Preise verwenden wir Decimal, um Fehler bei Fließkommazahlen zu vermeiden, daher wird unit_price als Zeichenkette übergeben. Die total() Methode addiert die Gesamtbeträge jeder Zeile mit einem Startwert von Decimal("0").

# src/shopcart/cli.py
from shopcart.order import Order


def main() -> None:
    order = Order(customer="Nguyễn Văn An")
    order.add("Cà phê sữa", "29000", 2)
    order.add("Bánh mì", "25000")
    print(f"Kunde: {order.customer}")
    print(f"Gesamtbetrag: {order.total():,} VND")

Die Datei src/shopcart/__init__.py kann leer bleiben.

4.4. Installation im editierbaren Modus

python -m pip install -e ".[dev]"
shopcart
Kunde: Nguyễn Văn An
Gesamtbetrag: 83.000 VND

Die Option -e (editable) verlinkt das Paket zum Quellcode, anstatt es in site-packages zu kopieren. Dadurch werden Änderungen an order.py sofort wirksam, ohne erneut zu installieren. Der Teil ".[dev]" bedeutet, das Projekt im aktuellen Verzeichnis mit der dev Gruppe zu installieren, sodass pytest und ruff ebenfalls installiert werden. Der Befehl shopcart ist im Terminal verfügbar dank der Deklaration in [project.scripts].

Hinweis: Einige Shells wie zsh interpretieren [...] als Glob-Muster. Daher solltest du ".[dev]" immer in Anführungszeichen setzen.

5. Tests schreiben und ausführen

# tests/test_order.py
from decimal import Decimal

import pytest

from shopcart.order import Order


def test_total_sums_all_lines():
    order = Order(customer="Trần Thị Bình")
    order.add("Trà đào", "35000", 2)
    order.add("Bánh flan", "15000", 3)

    assert order.total() == Decimal("115000")


def test_empty_order_has_zero_total():
    assert Order(customer="Lê Minh").total() == Decimal("0")


def test_add_rejects_non_positive_quantity():
    order = Order(customer="Phạm Hà")
    with pytest.raises(ValueError, match="quantity"):
        order.add("Nước suối", "10000", 0)
python -m pytest -q
...                                                     [100%]
3 passed in 0.02s

Tests importieren shopcart.order als installiertes Paket. Dies ist der Grund, warum du den pip install -e Schritt von oben brauchst. Wenn nicht installiert, wird das src layout einen ModuleNotFoundError auslösen, und dieser Fehler ist beabsichtigt: er zwingt dich, genau das zu testen, was Benutzer installieren werden. Der dritte Test nutzt pytest.raises, um zu prüfen, dass ungültige Mengen mit einer Nachricht abgelehnt werden, die “quantity” enthält.

6. Versionskontrolle: pyproject.toml oder requirements.txt?

Diese beiden Dateien schließen sich nicht gegenseitig aus. Sie dienen zwei unterschiedlichen Zwecken:

pyproject.toml Sperrdatei (requirements.txt mit fest gepinnten Versionen)
Zweck Beschreibt, was das Projekt braucht Reproduziert exakt eine Umgebung
Versionen Bereiche (>=2.31,<3) Fest (==2.32.3)
Verwendet für Bibliotheken und Anwendungen Anwendungen in Produktion (Server, Docker)

Bei einer Bibliothek, die auf PyPI veröffentlicht wird, brauchst du nur pyproject.toml. Wenn du Versionen fest pinnen würdest, würden Benutzer deiner Bibliothek Konflikte haben.

Bei einer Anwendung, die auf einem Server läuft, solltest du zusätzlich eine Sperrdatei haben, damit jeder Deployment exakt dieselbe Versionsmenge installiert:

python -m pip install -e .
python -m pip freeze --exclude-editable > requirements.lock

Bei der Bereitstellung installierst du zuerst aus der Sperrdatei, dann das Projekt selbst, um pip davon abzuhalten, Abhängigkeitsversionen automatisch zu aktualisieren:

python -m pip install -r requirements.lock
python -m pip install --no-deps .

Wenn du den Sperr-Prozess vereinfachen möchtest, kannst du pip-tools verwenden (Befehl pip-compile pyproject.toml) oder uv. Beide lesen direkt pyproject.toml und generieren eine Sperrdatei mit Hash-Werten.

7. Häufige Fehler

  • Pakete mit sudo pip install installieren. Dieser Befehl überschreibt System-Pakete und kann Systemtools beschädigen. Viele neue Linux-Distributionen blockieren dies sogar mit der Fehlermeldung externally-managed-environment. Die Lösung besteht darin, immer eine venv zu verwenden.
  • venv nicht aktivieren, bevor man installiert oder ausführt. Du kannst das mit which python wie in Abschnitt 2.2 überprüfen, oder den Python direkt aufrufen: .venv/bin/python -m pip ....
  • venv mit einer Python-Version erstellen und mit einer anderen ausführen. venv ist eng an den Interpreter gebunden, der sie erstellt hat. Wenn du Python aktualisierst (z.B. von 3.11 auf 3.12), lösche .venv und erstelle sie neu.
  • Projektverzeichnis verschieben oder umbenennen. Die Skripte in .venv/bin enthalten absolute Pfade und funktionieren nicht mehr nach dem Verschieben. Du musst die venv neu erstellen.
  • Alt setup.py mit pyproject.toml vermischen und Abhängigkeiten an beiden Stellen deklarieren. Wähle eine einzige Quelle der Wahrheit. Bei neuen Projekten ist das die Tabelle [project].
  • Entwicklungswerkzeuge in dependencies aufnehmen. pytest und ruff sollten in optional-dependencies (oder [dependency-groups] wenn dein Tool das unterstützt), damit Endbenutzer sie nicht installieren müssen.

Hinweis: Verwende keine venv, wenn du nur ein eigenständiges CLI-Tool wie black oder httpie auf dem ganzen System ausführen möchtest. In diesem Fall ist pipx besser: Es erstellt automatisch eine eigene venv für jedes Tool und fügt den Befehl zu PATH hinzu.

8. Fazit

Dieser Artikel hat drei Grundwerkzeuge für ein Python-Projekt behandelt:

  • python -m venv .venv isoliert Abhängigkeiten für jedes Projekt.
  • python -m pip installiert Pakete in den korrekten Interpreter.
  • pyproject.toml deklariert Metadaten, Abhängigkeiten, optionale Gruppen, CLI-Befehle und Werkzeugkonfiguration in einer einzigen Datei.

Mit src layout, pip install -e ".[dev]" und pytest hast du ein sauberes, leicht zu testendes und einfach zu teilendes Projekt. Für Anwendungen, die stabil bereitgestellt werden sollen, ergänze pyproject.toml durch eine Sperrdatei, um sicherzustellen, dass jede Umgebung dieselbe Versionsmenge installiert.

pythonpipvenvpyprojectpackaging