Cześć! W poprzedniej lekcji tworzyliśmy funkcje pomocnicze: przyjmowały argumenty, mogły mieć wartości domyślne i zwracały dane przez return. Teraz doprecyzujemy ich „umowę” z kodem wywołującym: jakie dane funkcja przyjmuje oraz jaki wynik ma zwrócić.
Adnotacje typów (type hints, type annotations) zwiększają czytelność kodu testowego i pozwalają IDE oraz narzędziom takim jak mypy zauważyć wiele pomyłek jeszcze przed uruchomieniem testów. Nauczysz się oznaczać parametry, wyniki funkcji oraz typowe struktury danych: listy, słowniki, krotki i precyzyjniej opisane słowniki TypedDict.
Adnotacja typu to informacja, nie blokada w czasie działania
W Pythonie typ zmiennej nie musi być deklarowany z góry. Możesz napisać:
status_code = 201
endpoint = "/users"
Python sam rozpoznaje, że status_code jest liczbą całkowitą (int), a endpoint napisem (str). Adnotacja dopisuje tę informację jawnie:
status_code: int = 201
endpoint: str = "/users"
Największa wartość adnotacji nie polega jednak na opisywaniu oczywistych lokalnych zmiennych. Jest szczególnie użyteczna na granicach kodu:
- w parametrach funkcji;
- w typach wartości zwracanych;
- w pustych kolekcjach, z których typ nie wynika;
- w strukturach danych przekazywanych między testami, klientami API i funkcjami pomocniczymi.
Ważne: Python standardowo nie sprawdza typów podczas uruchamiania programu. Ten kod jest uruchamialny:
def print_endpoint(endpoint: str) -> None:
print(endpoint)
print_endpoint(404)
Mimo że 404 nie jest napisem, interpreter Pythona po prostu go wypisze. Za to IDE lub statyczny kontroler typów powinny oznaczyć wywołanie jako podejrzane.
Dlatego adnotacje:
- nie zastępują asercji w testach;
- nie walidują automatycznie odpowiedzi HTTP;
- nie przyspieszają programu;
- stanowią kontrakt dla osoby czytającej kod oraz narzędzi analizujących go bez uruchamiania.
Obejrzyj krótkie fragmenty filmu “The BIGGEST Misconception About Type Hints In Python Explained” z kanału Indently. Pokazuje on zarówno składnię adnotacji, jak i kluczowe ograniczenie: Python sam nie wymusza ich w czasie wykonania.
The BIGGEST Misconception About Type Hints In Python Explained
Materiał wyjaśnia, dlaczego adnotacje poprawiają komunikację z IDE, oraz odróżnia ich rolę od faktycznej walidacji danych.
Zacznij od idei adnotacji: zwróć uwagę, że adnotacja deklaruje oczekiwanie wobec wartości i pozwala IDE podświetlić konflikt. Następnie pomiń przykład z klasą i obejrzyj funkcje i wyniki, aby utrwalić oznaczanie parametrów oraz wartości zwracanej. Zakończ fragmentem brak kontroli runtime, który wyjaśnia, dlaczego sam zapis typu nie zatrzyma błędnych danych podczas wykonania.
Oficjalna dokumentacja modułu typing potwierdza tę zasadę. Dokument pochodzi z wersji Python 3.7, więc w dalszej części lekcji użyjemy nowocześniejszej składni dostępnej w aktualnych wersjach Pythona, ale wyjaśnienie działania adnotacji pozostaje aktualne.
typing — Support for type hints — Python 3.7.17 - dokumentacja
Przeczytaj krótkie wprowadzenie z oficjalnej dokumentacji Pythona, aby zrozumieć, kto faktycznie korzysta z adnotacji typów.
W otwierającym opisie modułu, przed sekcją „Type aliases”, przeczytaj opis działania adnotacji. Skup się na rozróżnieniu między interpreterem Pythona a narzędziami zewnętrznymi, takimi jak IDE, lintery i type checkery.
Typowanie parametrów i wartości zwracanej
Składnia funkcji z adnotacjami ma dwa miejsca na opis typu:
def function_name(parameter: Type) -> ReturnType:
...
Dwukropek po nazwie parametru oznacza jego oczekiwany typ. Zapis po -> mówi, jaki typ ma zwrócić funkcja.
W automatyzacji przyda się na przykład funkcja budująca adres zasobu:
def build_user_endpoint(user_id: int) -> str:
return f"/users/{user_id}"
Jej podpis informuje, że:
user_idma być liczbą całkowitą;- funkcja zwraca napis;
- wynik będzie można przekazać dalej jako ścieżkę endpointu.
Użycie jest takie samo jak wcześniej:
endpoint = build_user_endpoint(42)
assert endpoint == "/users/42"
Adnotacje nie zmieniają sposobu wywoływania funkcji. Uzupełniają jedynie jej definicję.
Kilka parametrów i wartość domyślna
Typ zapisujesz przed wartością domyślną:
def build_request_options(
timeout_seconds: int = 10,
follow_redirects: bool = False,
) -> dict[str, int | bool]:
return {
"timeout_seconds": timeout_seconds,
"follow_redirects": follow_redirects,
}
To oznacza:
timeout_secondspowinien byćint;follow_redirectspowinien byćbool;- funkcja zwraca słownik o kluczach tekstowych, którego wartościami mogą być
intalbobool.
W tym przykładzie warto zwrócić uwagę na zapis int | bool. Pionowa kreska oznacza „jeden z tych typów”. Ta nowoczesna składnia działa od Python 3.10.
Dla funkcji, które wykonują działanie, ale nie zwracają użytecznej wartości, wpisz None:
def log_test_start(test_name: str) -> None:
print(f"Start testu: {test_name}")
-> None nie znaczy „zapomniałem zwrócić wynik”. To świadoma informacja: funkcja służy do wykonania efektu, tutaj wypisania komunikatu, a nie do obliczenia wyniku.
def expected_status_for(role: str) -> int:
statuses: dict[str, int] = {
"viewer": 200,
"admin": 200,
"anonymous": 401,
}
return statuses[role]
Taki podpis pozwala od razu zrozumieć przeznaczenie funkcji bez studiowania jej wnętrza: przyjmuje rolę tekstową i wyznacza oczekiwany kod HTTP.
Przeczytaj teraz dwie części Type hints cheat sheet z dokumentacji mypy. To praktyczna ściąga składni, którą warto później zachować jako punkt odniesienia.
Type hints cheat sheet - mypy Documentation - Read the Docs
Ściąga mypy zestawia współczesne adnotacje dla kolekcji i funkcji w krótkich przykładach. Nie musisz zapamiętywać wszystkich wariantów; skoncentruj się na tych, które będą codziennie występować w testach.
W sekcji „Useful built-in types” przeczytaj typy w kolekcjach. Zauważ różnicę między listą, słownikiem i krotką o ustalonej strukturze. Następnie przejdź do sekcji „Functions” i przeczytaj podstawy podpisów funkcji. Szczególnie przyjrzyj się -> None oraz temu, że typ parametru występuje przed jego wartością domyślną.
Typy struktur danych w testach
W poprzedniej lekcji poznaliśmy kolekcje. Teraz dodamy informację o typie ich elementów.
W aktualnym Pythonie, od wersji 3.9, używa się zwykle wbudowanych nazw typów:
test_emails: list[str] = [
"ania@example.test",
"piotr@example.test",
]
allowed_roles: set[str] = {
"viewer",
"editor",
"admin",
}
headers: dict[str, str] = {
"Accept": "application/json",
"X-Test-Run": "smoke",
}
response_summary: tuple[int, str] = (
201,
"Created",
)
Każdy zapis odpowiada na nieco inne pytanie:
| Adnotacja | Znaczenie | Przykład testowy |
|---|---|---|
list[str] | Lista napisów. | Adresy e-mail użytkowników testowych. |
set[str] | Zbiór unikalnych napisów. | Role dostępne w systemie. |
dict[str, str] | Słownik, gdzie klucze i wartości są napisami. | Nagłówki HTTP. |
tuple[int, str] | Krotka z dokładnie dwoma elementami, kolejno int i str. | Kod statusu oraz jego opis. |
tuple[int, ...] | Krotka o dowolnej długości, zawierająca tylko liczby całkowite. | Lista kodów statusu zapisana jako krotka. |
W typach kolekcji nawiasy kwadratowe opisują zawartość, a nie sam kontener. Zatem:
expected_statuses: list[int] = [200, 201, 204]
nie oznacza „lista lub liczba”. Oznacza „lista, której każdy element ma być liczbą całkowitą”.
Kiedy warto dopisać typ zmiennej?
Python i mypy często potrafią wywnioskować typ z przypisanej wartości:
expected_status = 201
Dopisywanie : int wszędzie byłoby więc hałaśliwe. Adnotuj przede wszystkim dane, których typ nie wynika naturalnie albo których znaczenie ma znaczenie dla kontraktu.
Klasyczny przypadek to pusta kolekcja:
validation_errors: list[str] = []
Bez adnotacji narzędzie nie wie jeszcze, jakie elementy planujesz później dodać. Dzięki niej wykryje próbę wstawienia wartości o niewłaściwym typie:
validation_errors.append("Brakuje pola email")
# validation_errors.append(404)
# Type checker powinien zgłosić konflikt: oczekiwano str, otrzymano int.
Słownik ogólny a słownik o określonym kształcie
Adnotacja dict[str, str | bool] bywa użyteczna, ale opisuje tylko typy kluczy i wartości:
payload: dict[str, str | bool] = {
"email": "ania@example.test",
"role": "viewer",
"active": True,
}
Nie mówi jednak nic o znaczeniu poszczególnych kluczy. Dla type checkera poniższy zapis nadal jest formalnie dopuszczalny, ponieważ False należy do typu str | bool:
payload["email"] = False
Dla danych API byłaby to pomyłka. Pole email ma być tekstem niezależnie od tego, że inne pole, active, rzeczywiście jest logiczne. Gdy struktura ma znane, stałe pola, zastosuj TypedDict.
from typing import TypedDict
class UserPayload(TypedDict):
email: str
role: str
active: bool
UserPayload opisuje słownik o trzech wymaganych polach:
emailmusi mieć wartośćstr;rolemusi mieć wartośćstr;activemusi mieć wartośćbool.
Możesz użyć go jako typu zwracanego przez funkcję przygotowującą dane testowe:
from typing import TypedDict
class UserPayload(TypedDict):
email: str
role: str
active: bool
def build_user_payload(
email: str,
role: str = "viewer",
active: bool = True,
) -> UserPayload:
return {
"email": email,
"role": role,
"active": active,
}
Kod testu pozostaje naturalny:
payload = build_user_payload(
email="tester@example.test",
role="editor",
)
assert payload["email"] == "tester@example.test"
assert payload["role"] == "editor"
assert payload["active"] is True

Adnotacja pozwala narzędziu odszukać problem jeszcze przed wysłaniem żądania:
invalid_payload: UserPayload = {
"email": "tester@example.test",
"role": "viewer",
"active": "yes",
}
Wartość "yes" jest napisem, a nie bool, więc IDE lub mypy powinny zgłosić błąd. Podobnie będzie w przypadku pominięcia wymaganego klucza, np. role.
TypedDict nie tworzy nowego rodzaju słownika działającego w runtime. Nadal korzystasz ze zwykłego słownika i indeksowania przez payload["email"]. Jego główną rolą jest opis struktury dla type checkera.
Nie traktuj też TypedDict jako walidatora danych z zewnętrznego API. Serwer nadal może przysłać niepoprawny JSON. Adnotacja opisuje to, czego oczekuje Twój kod; rzeczywistą odpowiedź trzeba sprawdzić asercjami, a później także walidacją schematu JSON.
Wartość może być None
Czasem wynik nie zawsze istnieje. Przykładowo funkcja może szukać nagłówka, który nie musi być obecny:
def get_request_id(headers: dict[str, str]) -> str | None:
return headers.get("X-Request-ID")
Typ str | None mówi: funkcja zwraca napis albo wartość None. Kod wywołujący powinien obsłużyć obie możliwości:
request_id = get_request_id(headers)
if request_id is not None:
print(f"Request ID: {request_id}")
Nie myl tego z parametrem domyślnym. Parametr może być opcjonalny w wywołaniu, ale nie musi przyjmować None:
def build_user_payload(
email: str,
role: str = "viewer",
) -> UserPayload:
...
role można pominąć, ponieważ ma wartość domyślną, lecz jeśli zostanie podany, powinien być napisem. Dopiero gdy None jest dozwoloną wartością, zapisujesz str | None.
W starszym kodzie możesz spotkać równoważny zapis:
from typing import Optional
request_id: Optional[str] = None
W nowym Pythonie zapis str | None jest zwykle krótszy i czytelniejszy.
Praktyczny standard dla kodu testowego
Na tym etapie stosuj prostą zasadę: adnotuj publiczne funkcje pomocnicze i dane przekazywane między częściami frameworka, ale nie komplikuj typów na siłę.
Dobry, proporcjonalny zestaw wygląda tak:
from typing import TypedDict
class UserPayload(TypedDict):
email: str
role: str
active: bool
def build_user_payload(
email: str,
role: str = "viewer",
active: bool = True,
) -> UserPayload:
return {
"email": email,
"role": role,
"active": active,
}
def expected_user_fields() -> set[str]:
return {"email", "role", "active"}
def has_expected_fields(response_body: dict[str, object]) -> bool:
return expected_user_fields().issubset(response_body)
Zauważ cel każdego typu:
UserPayloadprecyzyjnie opisuje dane, które sam tworzy framework testowy;set[str]opisuje zbiór nazw pól;dict[str, object]pasuje do bardziej niepewnej odpowiedzi z zewnętrznego systemu, której wartości mogą mieć różne typy.
Unikaj automatycznego używania Any, gdy typ wydaje się niewygodny. Any zasadniczo wyłącza wiele kontroli type checkera. Lepiej zacząć od prostego, uczciwego typu, a wraz z rosnącą wiedzą o danych stopniowo go doprecyzować.
Jeśli dodasz mypy do zależności deweloperskich projektu, możesz analizować plik bez uruchamiania testów:
mypy tests/test_users.py
W praktyce warto korzystać z obu warstw kontroli:
- type checker sprawdza zgodność podpisów, typów i struktur w kodzie;
- pytest uruchamia scenariusze oraz asercje przeciwko rzeczywistemu zachowaniu aplikacji.
Najważniejsze wnioski:
- Parametr oznaczasz przez
nazwa: Typ, a typ wyniku funkcji przez-> Typ. - Dla funkcji bez znaczącej wartości zwracanej stosuj
-> None. - W aktualnym Pythonie używaj między innymi
list[str],dict[str, str],set[str]ituple[int, str]. str | Noneoznacza wartość, która może być napisem alboNone.TypedDictjest właściwym wyborem dla słowników danych testowych o stałych, znanych polach.- Adnotacje są kontraktem analizowanym przez IDE i type checkery; nie zastępują testów ani walidacji danych w runtime.
W następnej lekcji przejdziesz od pojedynczych funkcji do organizacji kodu wielokrotnego użytku w moduły i pakiety Pythona.
Can't find a good explanation? Sign up and we'll make it for you
Sign up