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.
@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
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)
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)
Ü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
@
Ü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)
Ü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.
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)
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
Hinweis 1
Setzen Sie zweimal @overload und ein def mit demselben Namen über die Implementierung. Die Rümpfe der Stubs sind nur ...
Hinweis 2
Schreiben Sie im zweiten Stub None, nicht NoneType: def to_int(value: None) -> None: ...
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):
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
Hinweis 1
Nur die Rückgabe-Annotation von is_text_list ändert sich: -> TypeIs[list[str]].
Hinweis 2
Fügen Sie nach dem Listenfall if isinstance(value, int): return f"{value:,}" ein.
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):
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].
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
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.
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.
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.