typing

Adnotacje typów: opis, jakich wartości oczekuje funkcja i co zwraca. Sprawdzają je edytory i narzędzia takie jak mypy.

Na tej stronie

Przykład

#
Python
def average(scores: list[int]) -> float:
    return sum(scores) / len(scores)

def find_points(ranking: dict[str, int], name: str) -> int | None:
    return ranking.get(name)

ranking: dict[str, int] = {"Ania": 250, "Kuba": 80}

print(average([80, 90, 100]))
print(find_points(ranking, "Ania"))
print(find_points(ranking, "Ola"))
print(average([1.5, 2.5]))
Wynikzapisany wynik, możesz go sprawdzić
90.0
250
None
2.0

Ostatnie wywołanie przekazuje liczby zmiennoprzecinkowe, choć adnotacja mówi list[int]. Python wykona je bez błędu, a mypy zgłosiłby niezgodność typów.

Definicja i zastosowanie

#

Adnotacje typów to opis dopisany do kodu: def award(name: str, exp: int = 10) -> str: mówi, że funkcja przyjmuje napis i liczbę, a zwraca napis. Python nie sprawdza ich podczas działania programu. Korzystają z nich edytory, które podpowiadają kod i podkreślają błędy, oraz narzędzia takie jak mypy czy pyright, wykrywające pomyłki, zanim uruchomisz program.

Typy kolekcji zapisuje się wbudowanymi nazwami z nawiasami kwadratowymi: list[int], dict[str, int], tuple[int, int]. Wartość jednego z kilku typów opisuje operator |, np. int | str, a str | None oznacza napis albo None. To nowsze formy zapisów Union[int, str] i Optional[str] z modułu typing, które nadal spotkasz w starszym kodzie.

Moduł typing dostarcza też narzędzi do bardziej złożonych przypadków. TypedDict opisuje słownik o ustalonych kluczach, np. rekord odczytany z JSON-a. Protocol określa, jakie metody musi mieć obiekt, bez wymogu dziedziczenia, dzięki czemu narzędzia sprawdzą duck typing jeszcze przed uruchomieniem programu. Literal zawęża wartości do podanych, a Any wyłącza sprawdzanie.

Adnotacje są dostępne także w czasie działania programu, z czego korzystają @dataclass, pydantic czy FastAPI. Nie musisz typować wszystkiego od razu: zacznij od parametrów i wyników funkcji, bo typy zmiennych lokalnych narzędzia zwykle wywnioskują same.

Składnia

#
Składnia
from typing import Literal, Protocol, TypedDict

def funkcja(parametr: typ, opcjonalny: typ | None = None) -> typ_wyniku:
    ...

zmienna: dict[str, int] = {}

Najczęstsze zapisy

#
  • list[int]

    Python 3.9

    Lista liczb całkowitych. Tak samo zapisuje się set[str], tuple[int, str] i dict[str, int] (typ kluczy i wartości).
  • int | str

    Python 3.10

    Jeden z kilku typów. Starszy zapis: Union[int, str].
  • str | None

    Python 3.10

    Napis albo None. Starszy zapis: Optional[str].
  • Literal[...]

    Python 3.8

    Tylko wymienione wartości, np. Literal["easy", "hard"].
  • TypedDict

    Python 3.8

    Słownik o ustalonych kluczach i typach wartości.
  • Protocol

    Python 3.8

    Zestaw metod, które obiekt musi mieć, bez wymogu dziedziczenia.
  • Callable[[int], str]

    Python 3.5

    Funkcja, która przyjmuje int i zwraca str. Importowana z collections.abc albo typing.
  • Any

    Python 3.5

    Dowolny typ. Wyłącza sprawdzanie dla danej wartości.
  • type Alias = ...

    Python 3.12

    Nazwany alias typu, np. type Scores = list[int].

Więcej przykładów

#
TypedDict: słownik o znanych kluczach
Python
from typing import NotRequired, TypedDict

class StudentData(TypedDict):
    name: str
    world: int
    badges: list[str]
    mentor: NotRequired[str]

def describe(student: StudentData) -> str:
    mentor = student.get("mentor", "brak")
    return f"{student['name']} (świat {student['world']}), mentor: {mentor}"

ania: StudentData = {"name": "Ania", "world": 7, "badges": ["Start"]}
print(describe(ania))
print(type(ania).__name__)
Wynikzapisany wynik, możesz go sprawdzić
Ania (świat 7), mentor: brak
dict

W czasie działania programu to zwykły słownik. NotRequired oznacza klucz, którego może brakować.

Protocol: wymagane metody bez dziedziczenia
Python
from typing import Protocol

class Scorable(Protocol):
    def score(self) -> int: ...

class Quiz:
    def __init__(self, correct: int) -> None:
        self.correct = correct

    def score(self) -> int:
        return self.correct * 10

class Project:
    def score(self) -> int:
        return 50

def total(items: list[Scorable]) -> int:
    return sum(item.score() for item in items)

print(total([Quiz(3), Project(), Quiz(1)]))
Wynikzapisany wynik, możesz go sprawdzić
90

Quiz i Project nie dziedziczą po Scorable, ale mają metodę score(), więc narzędzia do sprawdzania typów je zaakceptują.

Union, Optional i Literal
Python
from typing import Literal, Optional, Union

Difficulty = Literal["easy", "medium", "hard"]

def exp_for(difficulty: Difficulty) -> int:
    return {"easy": 5, "medium": 10, "hard": 20}[difficulty]

def parse_points(value: Union[int, str]) -> Optional[int]:
    if isinstance(value, int):
        return value
    return int(value) if value.isdigit() else None

print(exp_for("hard"))
print(parse_points(40), parse_points("75"), parse_points("dużo"))
Wynikzapisany wynik, możesz go sprawdzić
20
40 75 None

W nowym kodzie Union[int, str] zapisuje się jako int | str, a Optional[int] jako int | None.

Dobre praktyki

#
  • Adnotacje nie zatrzymają błędnych danych w czasie działania. Uruchamiaj mypy albo pyright, np. w edytorze, a dane z zewnątrz waliduj osobno.
  • W nowym kodzie pisz list[int] i int | None zamiast List[int] i Optional[int] z modułu typing. Starsze zapisy nadal działają.
  • Złożony typ, który powtarza się w wielu miejscach, nazwij aliasem: type Ranking = dict[str, int] od Pythona 3.12, a w starszych wersjach zwykłym przypisaniem Ranking = dict[str, int].

Powiązane hasła

#

Widzisz błąd albo brakuje przykładu? Napisz do nas.