Zum Inhalt springen
aviral gupta

// A2.5 · ca. 30 Min. · Vertiefung

Overloads und Type Narrowing

Nach dieser Lektion geben Sie einer Funktion mit @overload eine Signatur je Argumenttyp, sagen voraus, wie mypy eine Union nach isinstance, None-Prüfungen und match eingrenzt, und schreiben ein TypeIs-Prädikat, das beide Zweige eingrenzt.

Lektion 5 von 6 in A2 Typisierung

Danach können Sie

  • @overload-Signaturen mit einer Implementierung schreiben und die Fehler von mypy dazu lesen
  • Vorhersagen, wie mypy eine Union nach isinstance, is None, Wahrheitstests, type() und match eingrenzt
  • Prädikatfunktionen mit TypeIs schreiben und wissen, wann stattdessen TypeGuard nötig ist
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Lektion A2.1: find liefert einen int oder None. Welchen Fehlercode meldet mypy für die letzte Zeile?

    def find(items: list[str], name: str) -> int | None:
        return items.index(name) if name in items else None
    
    
    print(find(["a"], "a") + 1)
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: double(3) liefert zur Laufzeit 6. Welchen Fehler meldet mypy für die letzte Zeile?

    def double(value: int | str) -> int | str:
        return value * 2
    
    
    print(double(3) + 1)
  3. Üben · Aufgabe 3 von 7

    Der zweite Stub hat ihn schon: Ergänzen Sie den Decorator, der double eine Signatur je Argumenttyp gibt.

    @____
    def double(value: int) -> int: ...
    @overload
    def double(value: str) -> str: ...
    def double(value: int | str) -> int | str:
        return value * 2
    @
  4. Üben · Aufgabe 4 von 7

    Welchen Typ zeigt mypy für v an?

    def f(v: int | str | None) -> None:
        if isinstance(v, int):
            pass
        elif v is None:
            pass
        else:
            reveal_type(v)
  5. Üben · Aufgabe 5 von 7

    v ist int | float | str, w ist str | None und u ist int | str; is_str liefert TypeIs[str] und maybe_str liefert TypeGuard[str]. Ordnen Sie jeder Prüfung den Typ zu, den mypy innerhalb ihres if annimmt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. is_short liefert False für lange Strings. Welchen Typ zeigt mypy im else-Zweig an?

    from typing import TypeIs
    
    
    def is_short(value: object) -> TypeIs[str]:
        return isinstance(value, str) and len(value) < 5
    
    
    def show(value: str | int) -> None:
        if is_short(value):
            print("short text", value)
        else:
            reveal_type(value)
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie pick(items, where) für eine list[str]: Ein int-Index liefert einen str, ein slice eine list[str]. Geben Sie ihr zwei @overload-Stubs und eine Implementierung, sodass pick(names, 0).upper() und len(pick(names, slice(1, 3))) mypy --strict bestehen. Geben Sie beides für names = ["Ada", "Alan", "Grace"] aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Overloads und Narrowing zusammen

double hat zwei Overloads, deshalb besteht double(value) + 1 die Typprüfung, sobald value als int bekannt ist. is_pair ist ein TypeIs-Prädikat. describe grenzt seine vierteilige Union Schritt für Schritt ein: is None, dann is_pair, dann isinstance, und was übrig bleibt, muss ein str sein. Das Programm besteht mypy --strict.

main.py

from typing import TypeIs, get_overloads, overload


@overload
def double(value: int) -> int: ...
@overload
def double(value: str) -> str: ...
def double(value: int | str) -> int | str:
    return value * 2


def is_pair(value: object) -> TypeIs[tuple[int, int]]:
    return isinstance(value, tuple) and len(value) == 2 and all(isinstance(x, int) for x in value)


def describe(value: int | str | tuple[int, int] | None) -> str:
    if value is None:
        return "nothing"
    if is_pair(value):
        left, right = value
        return f"pair summing to {left + right}"
    if isinstance(value, int):
        return f"number {double(value) + 1}"
    return f"text {double(value).upper()}"


for value in (20, "ab", (3, 4), None):
    print(describe(value))
print(len(get_overloads(double)))

Ausführen mit

python main.py

Ausgabe

number 41
text ABAB
pair summing to 7
nothing
2
  • double(value) + 1 und double(value).upper() wählen jeweils den passenden Overload; mit nur der Union-Signatur wären beide Zeilen Fehler.
  • is_pair liefert True für jedes tuple[int, int]; das macht seinen False-Zweig sicher.
  • Nach den drei Prüfungen weiß mypy, dass die letzte Zeile nur einen str sieht.
  • get_overloads(double) liefert die beiden Stubs; die Implementierung gehört nicht dazu.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Ein überladener Konverter

to_int wandelt einen str in einen int um und reicht None durch. Mit der Union-Signatur lehnt mypy total() ab, obwohl dort nur Strings übergeben werden. Setzen Sie zwei @overload-Stubs, str zu int und None zu None, über die Implementierung, sodass total() die Typprüfung besteht. Die Implementierung bleibt, wie sie ist.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Setzen Sie zweimal @overload und ein def mit demselben Namen über die Implementierung. Die Rümpfe der Stubs sind nur ...

  2. Hinweis 2

    Schreiben Sie im zweiten Stub None, nicht NoneType: def to_int(value: None) -> None: ...

  3. Hinweis 3

    Die Implementierung behält ihre Union-Signatur und keinen Decorator; sie muss die letzte Definition sein.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from typing import overload


@overload
def to_int(value: str) -> int: ...
@overload
def to_int(value: None) -> None: ...
def to_int(value: str | None) -> int | None:
    if value is None:
        return None
    return int(value)


def total(texts: list[str]) -> int:
    return sum(to_int(text) for text in texts)
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from typing import overload


def to_int(value: str | None) -> int | None:
    if value is None:
        return None
    return int(value)


def total(texts: list[str]) -> int:
    return sum(to_int(text) for text in texts)

test_main.py

from typing import get_overloads, get_type_hints

from main import to_int, total


def test_overloads():
    """to_int hat zwei Overloads über der Implementierung: str zu int und None zu None"""
    stubs = [stub for stub in get_overloads(to_int) if stub.__code__.co_firstlineno < to_int.__code__.co_firstlineno]
    got = [(get_type_hints(stub)["value"], get_type_hints(stub)["return"]) for stub in stubs]
    assert sorted(got, key=repr) == sorted([(str, int), (type(None), type(None))], key=repr), f"die Overloads sind {got!r}"


def test_values():
    """to_int("42") ist 42 und to_int(None) ist None"""
    got = to_int("42"), to_int(None)
    assert got == (42, None), f"to_int lieferte {got!r}"


def test_total():
    """total addiert die Zahlen"""
    got = total(["1", "2", "39"])
    assert got == 42, f"total lieferte {got!r}"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 2

Konfigurationswerte darstellen

Ein Konfigurationswert ist ein str, ein int oder eine list[str]. Ändern Sie is_text_list so, dass es TypeIs[list[str]] statt TypeGuard liefert, damit beide Zweige eingegrenzt werden. Vervollständigen Sie dann render: Eine Liste wird mit ", " verbunden, ein int mit Tausendertrennzeichen formatiert (f"{value:,}"), und ein str wird unverändert zurückgegeben, ohne str()-Aufruf: mypy soll wissen, dass es ein str ist.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Nur die Rückgabe-Annotation von is_text_list ändert sich: -> TypeIs[list[str]].

  2. Hinweis 2

    Fügen Sie nach dem Listenfall if isinstance(value, int): return f"{value:,}" ein.

  3. Hinweis 3

    Mit TypeGuard wäre return value am Ende ein Fehler, weil mypy noch str | int | list[str] sähe. Mit TypeIs bleibt nur str übrig.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from typing import TypeGuard, TypeIs

type Value = str | int | list[str]


def is_text_list(value: Value) -> TypeIs[list[str]]:
    return isinstance(value, list)


def render(value: Value) -> str:
    if is_text_list(value):
        return ", ".join(value)
    if isinstance(value, int):
        return f"{value:,}"
    return value
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from typing import TypeGuard, TypeIs

type Value = str | int | list[str]


def is_text_list(value: Value) -> TypeGuard[list[str]]:
    return isinstance(value, list)


def render(value: Value) -> str:
    if is_text_list(value):
        return ", ".join(value)
    return str(value)

test_main.py

from typing import TypeIs, get_origin, get_type_hints

from main import is_text_list, render


def test_typeis():
    """is_text_list ist mit der Rückgabe TypeIs[...] annotiert"""
    got = get_type_hints(is_text_list)["return"]
    assert get_origin(got) is TypeIs, f"is_text_list liefert {got!r}, erwartet TypeIs[list[str]]"


def test_list():
    """Eine Liste wird mit Komma und Leerzeichen verbunden"""
    got = render(["a", "b"])
    assert got == "a, b", f"render(['a', 'b']) lieferte {got!r}"


def test_int():
    """Ein int bekommt Tausendertrennzeichen"""
    got = render(1234567)
    assert got == "1,234,567", f"render(1234567) lieferte {got!r}"


def test_str():
    """Ein str kommt unverändert zurück"""
    got = render("debug")
    assert got == "debug", f"render('debug') lieferte {got!r}"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Overload-Stubs ohne Implementierung

from typing import overload


@overload
def double(value: int) -> int: ...
@overload
def double(value: str) -> str: ...


print(double(3))

Was Python ausgibt

NotImplementedError: You should not call an overloaded function. A series of @overload-decorated functions outside a stub module should always be followed by an implementation that is not @overload-ed.

Warum, und die Lösung

Die Stubs sind nur Signaturen. Fügen Sie nach den Stubs eine Definition ohne @overload hinzu, die jeden Fall behandelt: def double(value: int | str) -> int | str: return value * 2. mypy meldet die fehlende Implementierung ebenfalls: An overloaded function outside a stub file must have an implementation [no-overload-impl].

Eine Implementierung, die einen Fall auslässt

from typing import overload


@overload
def to_int(value: str) -> int: ...
@overload
def to_int(value: None) -> None: ...
def to_int(value: str | None) -> int | None:
    return int(value)


print(to_int(None))

Was Python ausgibt

TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'

Warum, und die Lösung

Die Overloads versprechen, dass to_int(None) None liefert, aber die Implementierung prüft das nie. Grenzen Sie zuerst ein: if value is None: return None, dann return int(value). mypy erkennt das: Es meldet die return-Zeile mit einem [arg-type]-Fehler, weil int() kein str | None annimmt.

Eine TypeIs-Funktion, die für manche Treffer False liefert

from typing import TypeIs


def is_short(value: object) -> TypeIs[str]:
    return isinstance(value, str) and len(value) < 5


def show(value: str | int) -> None:
    if is_short(value):
        print("short text", value)
    else:
        print(value + 1)


show(41)
show("a longer text")

Was Python ausgibt

TypeError: can only concatenate str (not "int") to str

Warum, und die Lösung

mypy akzeptiert value + 1, weil TypeIs[str] sagt, dass False "kein str" bedeutet. is_short liefert aber auch für lange Strings False, damit ist das Versprechen gebrochen. Halten Sie das Prädikat exakt (return isinstance(value, str)) und prüfen Sie die Länge getrennt, oder nutzen Sie eine einfache bool-Rückgabe, die nichts eingrenzt.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

@overload: eine Signatur je Fall

def double(value: int | str) -> int | str verliert den Zusammenhang zwischen Argument und Ergebnis: double(3) + 1 ist ein Fehler, weil das Ergebnis ein str sein könnte. Overloads stellen ihn wieder her. Schreiben Sie je Fall einen @overload-Stub, der mit ... endet, dann genau eine Implementierung ohne Decorator. mypy nimmt den ersten passenden Stub; ausgeführt wird die Implementierung, und sie muss die Argumente aller Stubs annehmen. typing.get_overloads() listet die Stubs zur Laufzeit.

Narrowing: mypy folgt Ihren Prüfungen

In if isinstance(value, int): behandelt mypy einen Wert vom Typ int | str als int, im else-Zweig als str. Das gilt auch für value is None, für match value: case str(): und für Wahrheitstests: Nach if not text: ist ein str | None nur noch Literal[''] | None. type(value) is int grenzt nur den if-Zweig ein, weil eine Unterklasse wie bool in den else-Zweig gelangt. Narrowing ist statische Analyse: Zur Laufzeit wird nichts umgewandelt oder geprüft.

TypeIs: eine eigene Narrowing-Funktion

Eine Funktion, die bool liefert, sagt mypy nichts. Annotieren Sie die Rückgabe als TypeIs[str], und ein Aufruf im if grenzt ein wie isinstance: auf str bei True, ohne str bei False. Der eingegrenzte Typ muss ein Subtyp des Parametertyps sein. TypeGuard[list[str]] erlaubt, list[object] auf list[str] einzugrenzen, grenzt aber nur den True-Zweig ein. Beide sind Versprechen: Liefert die Funktion für einen str False, ist der else-Zweig falsch.

Quellen

Zuletzt geprüft am 29. September 2026