Files
it-know-how/python/mypy.md
T
2026-03-27 12:58:01 +01:00

15 KiB
Executable File
Raw Blame History

Mypy ist ein statischer Typprüfer („Type Checker“) für Python. Er hilft dir, Fehler schon beim Schreiben bzw. vor dem Ausführen des Codes zu finden ähnlich wie ein Compiler in streng typisierten Sprachen ohne dass du Python als dynamische Sprache „aufgibst“.

Ich gehe Schritt für Schritt durch:

  1. Grundidee (für Einsteiger verständlich)
  2. Kurzer praktischer Einstieg (Installation, erste Checks, einfache Beispiele)
  3. Welche Probleme mypy löst
  4. Abgrenzung zu ähnlichen / verwandten Tools
  5. Herausforderungen und typische Stolpersteine
  6. Praxisnahe Beispiele und Patterns

1. Grundidee: Was macht mypy?

Python ist dynamisch typisiert: Variablen haben zur Laufzeit Typen, aber der Interpreter prüft sie nicht im Voraus. Viele Fehler sieht man erst, wenn der entsprechende Code ausgeführt wird.

Mypy ändert daran nichts zur Laufzeit aber es analysiert deinen Code statisch (also ohne ihn auszuführen) und prüft, ob die verwendeten Typannotationen konsistent sind.

Typannotationen ein Beispiel

Ohne Typen:

def add(a, b):
    return a + b

Das ist legal, aber du kannst aus dem Code nicht erkennen, ob a und b Zahlen, Strings oder etwas anderes sein sollen. Python lässt vieles zu:

print(add(1, 2))       # 3
print(add("a", "b"))   # "ab"
print(add(1, "b"))     # TypeError zur Laufzeit

Mit Typannotationen:

def add(a: int, b: int) -> int:
    return a + b

Damit sagst du: add nimmt zwei int und gibt einen int zurück.

Mypy überprüft jetzt:

mypy mein_code.py

und meldet z.B.:

mein_code.py:10: error: Argument 2 to "add" has incompatible type "str"; expected "int"

wenn du irgendwo add(1, "b") aufrufst.

Grundidee:
Du schreibst „Verträge“ (Typen) in deinen Code, und mypy überprüft, ob du dich überall daran hältst. Das verbessert Lesbarkeit, Robustheit und macht refactoring sicherer.


2. Kurzer praktischer Einstieg

Installation

pip install mypy

(Je nach Setup evtl. in einer virtuellen Umgebung.)

Minimalbeispiel

calculator.py:

def add(a: int, b: int) -> int:
    return a + b

def main() -> None:
    x = add(1, 2)
    y = add("a", "b")  # Fehler

    print(x, y)

Mypy ausführen:

mypy calculator.py

Ausgabe:

calculator.py:6: error: Argument 1 to "add" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)

Obwohl Python den Code ausführen würde (und bei add("a", "b") sogar ein „korrektes“ Ergebnis liefern würde: "ab"), sagt mypy: Du hast gegen deinen eigenen Typvertrag verstoßen.

Gradual Typing

Du musst nicht alles von Anfang an typisieren. Du kannst Schritt für Schritt anfangen:

def add(a, b):  # keine Typen hier
    return a + b

def use_add() -> int:
    result = add(1, 2)   # mypy lässt das oft durchgehen (je nach Konfiguration)
    return result

Mypy arbeitet „gradual“:

  • Ungetypte Bereiche werden als Any betrachtet (unsicher, aber flexibel).
  • Getypte Bereiche werden überprüft.
  • Du kannst nach und nach mehr Typen hinzufügen und die Strenge erhöhen.

3. Welche Probleme werden durch mypy gelöst?

3.1. Typbezogene Fehler früh erkennen

Typische Klassen von Bugs:

  1. Falsche Argumenttypen:
def send_email(to: str, subject: str, body: str) -> None:
    ...

send_email(["user@example.com"], "Hi", "Text")  # Bug: Liste statt String

Mypy:

error: Argument 1 to "send_email" has incompatible type "List[str]"; expected "str"
  1. Verfügbare Attribute/Methoden:
def greet(name: str) -> None:
    print(name.upper())

user_name: int = 42
greet(user_name)

Mypy:

error: Argument 1 to "greet" has incompatible type "int"; expected "str"
  1. Optionale Werte vergessen zu prüfen (None):
from typing import Optional

def get_user_name(user_id: int) -> Optional[str]:
    ...

def print_name(user_id: int) -> None:
    name = get_user_name(user_id)
    print(name.upper())  # Bug: name kann None sein!

Mypy:

error: Item "None" of "Optional[str]" has no attribute "upper"

Du wirst gezwungen, zuerst auf None zu prüfen:

def print_name(user_id: int) -> None:
    name = get_user_name(user_id)
    if name is None:
        print("User not found")
        return
    print(name.upper())  # jetzt ok

3.2. Sicherere Refactorings

Wenn du Funktionensignaturen änderst, Parameter umbenennst oder Rückgabetypen anpasst, kann mypy dir helfen, alle Stellen zu finden, die du anpassen musst.

Beispiel:

# vorher
def get_price(product_id: int) -> float:
    ...

# nachher
def get_price(product_id: int) -> int:  # Rückgabetyp geändert!
    ...

Wenn irgendwo angenommen wird, dass float zurückkommt:

price_cents: float = get_price(123)  # jetzt inkonsistent

meldet mypy das. Das verringert das Risiko von subtilen Bugs nach Refactorings.

3.3. Bessere Dokumentation & IDE-Unterstützung

Typannotationen sind lebende Dokumentation:

def load_config(path: str) -> dict[str, str]:
    ...

Du siehst sofort, was die Funktion erwartet und liefert ohne lange Kommentare. IDEs nutzen die Typen für:

  • Autovervollständigung
  • Inlay Hints
  • Navigation („go to definition“)
  • Inline-Fehlermeldungen

4. Abgrenzung zu ähnlichen / verwandten Tools

4.1. Mypy vs. Linter (z.B. flake8, pylint)

Linter prüfen v.a.:

  • Stil (PEP 8)
  • potenziell problematische Patterns (unbenutzte Variablen, Schatten von Builtins, zu komplexe Funktionen)
  • gewisse Logikfehler (z.B. nie erreichte Codezweige)

Mypy fokussiert auf Typkonsistenz:

  • Stimmen die deklarierten Typen mit den tatsächlichen Verwendungen überein?
  • Können bestimmte Codezweige überhaupt erreicht werden, wenn Typen berücksichtigt werden?
  • Sind Operationen auf bestimmten Typen erlaubt?

Beispiel:

x = []
x.append(1)
x.append("a")

Linter: meistens kein Problem.
Mypy (je nach Typinferenz) könnte sagen:

List item 1 has incompatible type "str"; expected "int"

Fazit:
Linter und mypy ergänzen sich sie ersetzen sich nicht.

4.2. Mypy vs. Testframeworks (pytest, unittest)

Tests:

  • prüfen Laufzeitverhalten für konkrete Eingaben.
  • stellen sicher, dass Funktionen das tun, was fachlich / funktional gewünscht ist.

Mypy:

  • prüft nur Typkonsistenz keine fachliche Korrektheit.
  • findet z.B. nicht, ob du die falsche mathematische Formel verwendest, solange die Typen passen.

Beispiel:

def calculate_discount(price: float) -> float:
    return price * 2  # fachlich falsch, aber typgerecht

Mypy ist zufrieden. Ein Unit-Test würde diesen Fehler finden.

Fazit:
Mypy ergänzt Tests, ersetzt sie aber nicht.

4.3. Mypy vs. andere Typechecker (Pyright, Pyre, pytype)

Es gibt mehrere Typchecker für Python:

  • mypy der „Klassiker“, in Python geschrieben, von vielen Projekten verwendet.
  • pyright sehr schneller Typechecker (Microsoft), in TypeScript geschrieben.
  • pyre von Meta (Facebook), mit Fokus auf große Codebasen.
  • pytype von Google.

Sie verfolgen alle eine ähnliche Idee: statische Typprüfung für Python. Unterschiede gibt es bei:

  • Performance
  • Genauigkeit / Strenge in bestimmten Bereichen
  • Tooling-Integration (z.B. VS Code nutzt intern Pyright)

Für den Einstieg ist mypy völlig ausreichend und weit verbreitet.

4.4. Mypy vs. Laufzeit-Typprüfung (pydantic, marshmallow)

pydantic & Co.:

  • validieren und konvertieren Daten zur Laufzeit (z.B. JSON-Input in API).
  • werfen Exceptions, wenn Daten nicht passen.
  • nutzen Typannotationen als Basis, sind aber nicht auf Compile-/Check-Zeit beschränkt.

Mypy:

  • prüft nur zur Analysezeit, ändert das Laufzeitverhalten nicht.
  • „merkt nicht“, ob zur Laufzeit echte Validierung stattfindet.

Beispiel mit pydantic:

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str

user = User(id="123", name="Alice")  # zur Laufzeit wird "123" zu int geparst

Mypy würde melden:

Argument "id" to "User" has incompatible type "str"; expected "int"

obwohl pydantic das zur Laufzeit akzeptiert und konvertiert.
Hier musst du entscheiden, ob du dich eher am statischen Vertrag (Typ) oder am dynamischen Verhalten orientieren willst.


5. Herausforderungen und typische Stolpersteine

5.1. Legacy Code ohne Typen

In bestehendem Code fehlen oft Typannotationen, und vieles ist dynamisch.

Strategien:

  • Zuerst nur neue Modules/Funktionen typisieren.
  • Mypy mit „lockereren“ Einstellungen starten.
  • Langsam „strictness“ erhöhen.

Beispiel-Konfiguration (mypy.ini):

[mypy]
python_version = 3.11
ignore_missing_imports = True
disallow_untyped_defs = False
disallow_incomplete_defs = False

Später kannst du verschärfen:

disallow_untyped_defs = True
disallow_incomplete_defs = True
warn_unused_ignores = True
strict_optional = True

5.2. Dynamische Features von Python

Dinge wie:

  • dynamisches Hinzufügen von Attributen
  • setattr, getattr
  • Metaklassen-Magie
  • Monkey-Patching

sind schwer für statische Analyser.

Beispiel:

class Dynamic:
    pass

obj = Dynamic()
obj.name = "Alice"  # dynamisches Attribut
print(obj.name)

Mypy weiß nicht, dass name existiert, und meldet:

error: "Dynamic" has no attribute "name"

Workarounds:

  • Attribut im Klassendefinitionskörper deklarieren:

    class Dynamic:
        name: str
    
  • oder # type: ignore an problematischen Stellen nutzen.

5.3. Komplexe Typen und Verbosität

Generics, Union, Optional, TypedDict, Protocol etc. können komplex werden. Das kostet Einarbeitung.

Beispiel für generische Funktion:

from typing import TypeVar, Iterable, List

T = TypeVar("T")

def first(items: Iterable[T]) -> T:
    for item in items:
        return item
    raise ValueError("Empty iterable")

Mypy hilft hier, allgemeingültige, typsichere Utilities zu schreiben, aber das Typ-System wird relativ mächtig (und gelegentlich inelegant).

5.4. False Positives und # type: ignore

Manchmal weißt du mehr als mypy. Dann musst du mit mypy kommunizieren:

from typing import cast, Any

def get_from_json(json_obj: dict[str, Any]) -> int:
    return cast(int, json_obj["value"])

oder:

some_weird_library_call()  # type: ignore[arg-type]

Zu viele # type: ignore können aber wieder die Sicherheit untergraben. Es lohnt sich, sie sparsam und begründet einzusetzen.

5.5. Performance bei großen Codebasen

Für wirklich große Projekte kann mypy langsamer werden, vor allem bei vielen Imports und tiefen Typstrukturen.
Es gibt Optionen wie --incremental und dmypy (Daemon-Modus), um das zu beschleunigen.


6. Praxisnahe Beispiele & Patterns

6.1. Basic Typannotationen

def greet(name: str, times: int = 1) -> None:
    for _ in range(times):
        print(f"Hello, {name}!")

Sammlungstypen:

from typing import List, Dict

def total_length(names: List[str]) -> int:
    length = 0
    for n in names:
        length += len(n)
    return length

def invert_mapping(mapping: Dict[int, str]) -> Dict[str, int]:
    return {v: k for k, v in mapping.items()}

Ab Python 3.9 kannst du oft die Kurzform nutzen:

def total_length(names: list[str]) -> int:
    ...
def invert_mapping(mapping: dict[int, str]) -> dict[str, int]:
    ...

6.2. Optional und Union

from typing import Optional, Union

def parse_int(value: str) -> Optional[int]:
    try:
        return int(value)
    except ValueError:
        return None

def stringify(value: Union[int, float]) -> str:
    return f"{value:.2f}"

Aufruf:

result = parse_int("123")
if result is not None:
    print(result + 1)

Mypy zwingt dich, mit dem None-Fall umzugehen.

6.3. Typen für Klassen

class User:
    def __init__(self, user_id: int, name: str) -> None:
        self.user_id = user_id
        self.name = name

    def greet(self) -> str:
        return f"Hello, {self.name}!"

6.4. Dataclasses mit Typen

from dataclasses import dataclass

@dataclass
class Product:
    id: int
    name: str
    price_cents: int

def apply_discount(product: Product, percent: float) -> Product:
    discount = int(product.price_cents * percent / 100)
    return Product(
        id=product.id,
        name=product.name,
        price_cents=product.price_cents - discount
    )

Mypy prüft, ob du Product überall korrekt verwendest.

6.5. Typen für „Dictionary-Objekte“: TypedDict

Wenn du viele Dictionaries mit immer der gleichen Struktur verwendest (z.B. JSON):

from typing import TypedDict

class UserData(TypedDict):
    id: int
    name: str
    email: str

def send_welcome_mail(user: UserData) -> None:
    print(f"Sending mail to {user['email']}")

Mypy prüft:

  • fehlen Keys?
  • stimmen die Typen der Werte?

6.6. Duck Typing mit Protocol

Du willst „etwas, das wie eine Datei ist“, nicht unbedingt eine konkrete Klasse:

from typing import Protocol

class Writer(Protocol):
    def write(self, data: str) -> int:
        ...

def log_to(writer: Writer, message: str) -> None:
    writer.write(message + "\n")

Jede Klasse, die write(self, str) -> int implementiert, passt ganz im Sinne von „Duck Typing“, aber statisch überprüfbar.

6.7. Mypy-Konfiguration in der Praxis

mypy.ini im Projektverzeichnis:

[mypy]
python_version = 3.11
strict = True  # aktiviert viele strenge Checks

# Beispiele für häufige Optionen:
# disallow_untyped_defs = True
# disallow_incomplete_defs = True
# no_implicit_optional = True
# warn_unused_ignores = True
# warn_return_any = True

Du kannst auch per Modulbereich Regeln setzen:

[mypy-myproject.legacy.*]
ignore_errors = True  # Legacy-Code vorerst ignorieren

[mypy-myproject.core.*]
strict = True

Zusammenfassung

  • Grundidee: mypy ist ein statischer Typprüfer für Python. Du annotierst deinen Code mit Typen, und mypy überprüft, ob der Code diese Verträge einhält.
  • Probleme, die gelöst werden:
    • Frühes Finden von typbezogenen Fehlern
    • Sicherere Refactorings
    • Bessere Dokumentation und Tooling/IDE-Unterstützung
  • Abgrenzung:
    • Linter → Stil & allgemeine Codequalität; mypy → Typkonsistenz
    • Tests → fachliche Korrektheit zur Laufzeit; mypy → statische Typkonsistenz
    • Andere Typchecker (Pyright etc.) verfolgen die gleiche Grundidee
    • Runtime-Validatoren (pydantic) prüfen zur Laufzeit, mypy nur zur Analysezeit
  • Herausforderungen:
    • Einstieg in bestehende, dynamische Codebasen
    • Dynamische Python-Features sind schwer statisch zu erfassen
    • Komplexe Typen können verbose sein
    • Manchmal sind cast und # type: ignore nötig

Wenn du magst, kann ich dir als nächsten Schritt ein kleines Beispielprojekt skizzieren (z.B. eine Mini-API oder ein CLI-Tool) und zeigen, wie man mypy dort konkret einführt und schrittweise verschärft.