Cześć! W poprzedniej lekcji zbudowaliśmy klasy, które łączą stan i zachowanie, na przykład klienta API przechowującego base_url, token oraz limit czasu. Taki kod wkrótce będzie pobierał konfigurację, wysyłał żądania i przetwarzał odpowiedzi — a tam pewne niepowodzenia są przewidywalne: tekst może nie dać się zamienić na liczbę, może brakować oczekiwanego klucza albo dane testowe mogą nie spełniać reguł.
W tej lekcji nauczysz się rozpoznawać takie sytuacje i obsługiwać je przez wyjątki Pythona. Celem nie jest ukrywanie błędów za wszelką cenę, lecz wybieranie właściwej reakcji: bezpieczne odzyskanie kontroli, przekazanie jasnego komunikatu albo świadome przerwanie scenariusza testowego.
Czym jest wyjątek?
Wyjątek to zdarzenie podczas wykonywania programu, które przerywa jego zwykły tok. Python tworzy wtedy obiekt wyjątku zawierający między innymi jego typ i opis problemu.
Przykłady znanych wyjątków:
| Sytuacja | Typ wyjątku |
|---|---|
| Dzielenie przez zero | ZeroDivisionError |
int("abc") | ValueError |
| Odczyt nieistniejącego klucza słownika | KeyError |
| Odczyt indeksu spoza listy | IndexError |
| Użycie niezgodnych typów w operacji | TypeError |
Nie każdy wyjątek oznacza wadę implementacji. Jeśli użytkownik poda "zero" tam, gdzie potrzebna jest liczba, albo konfiguracja zawiera błędny limit czasu, program może i powinien przewidzieć taką możliwość. To właśnie dobry kandydat do obsługi wyjątku.
Natomiast nie należy mylić obsługi wyjątków z „wyciszaniem wszystkiego”. Nieoczekiwany AttributeError w kodzie klienta API często oznacza błąd programistyczny, który powinien pozostać widoczny w tracebacku i zostać naprawiony.
Obejrzyj pierwszy fragment filmu, aby zobaczyć podstawowy mechanizm try i except, a następnie różnicę między obsługą konkretnego typu błędu a zbyt ogólną reakcją.
12. Kurs Python 3 - obsługa wyjątków (try, except)
Film „12. Kurs Python 3 - obsługa wyjątków (try, except)” z kanału Kanał o Wszystkim pokazuje przebieg programu, gdy wyjątek pozostaje nieobsłużony, oraz składnię obsługi konkretnych błędów.
Obejrzyj podstawy try except, zwracając uwagę, że nieobsłużony wyjątek zatrzymuje wykonanie skryptu, a wyjątek obsłużony przekazuje sterowanie do bloku except. Następnie obejrzyj typy wyjątków, aby utrwalić, że różne problemy, takie jak dzielenie przez zero i niezgodność typów, mają różne klasy wyjątków.
try i except: przechwyć tylko to, czego się spodziewasz
Kod, który może zgłosić przewidywalny wyjątek, umieszczasz w bloku try. Bezpośrednio po nim podajesz jeden lub więcej bloków except, opisujących reakcję na konkretne typy problemów.
raw_timeout = "nieznany"
try:
timeout_seconds = float(raw_timeout)
except ValueError:
print("Limit czasu musi być liczbą, np. 5 lub 2.5.")
float("nieznany") zgłasza ValueError, więc Python opuszcza blok try i wykonuje dopasowany except.
Ważna reguła sterowania programem: gdy wyjątek wystąpi w try, pozostałe instrukcje tego bloku nie wykonają się.
try:
timeout_seconds = float("nieznany")
print("Ta linia się nie wykona.")
except ValueError:
print("Nie można odczytać limitu czasu.")
W automatyzacji testów najczęściej chcesz wskazać konkretny typ wyjątku:
def get_user_name(user: dict[str, str]) -> str:
try:
return user["name"]
except KeyError:
return "Nieznany użytkownik"
Ten wariant jest poprawny tylko wtedy, gdy brak nazwy jest rzeczywiście akceptowalną sytuacją biznesową i wartość zastępcza ma sens. Jeżeli name jest obowiązkowym polem odpowiedzi API, zwrócenie fikcyjnej wartości mogłoby ukryć defekt usługi. W takim teście lepiej pozwolić wyjątkowi przerwać wykonanie lub zgłosić własny, bardziej zrozumiały błąd.
Możesz obsłużyć kilka typów w osobnych blokach:
def read_retry_count(config: dict[str, str]) -> int:
try:
raw_value = config["retry_count"]
except KeyError:
return 0
try:
return int(raw_value)
except ValueError:
return 0
Tutaj dwie sytuacje mają różne przyczyny:
KeyErroroznacza, że kluczaretry_countw ogóle nie ma;ValueErroroznacza, że klucz istnieje, ale jego wartość nie jest poprawną liczbą.
Jeżeli reakcja ma być identyczna, typy można połączyć:
try:
timeout_seconds = float(config["timeout_seconds"])
except (KeyError, ValueError):
timeout_seconds = 10.0
Taki zapis jest sensowny, gdy brak wartości i zły format rzeczywiście prowadzą do tej samej, bezpiecznej decyzji. W przypadku krytycznej konfiguracji testów, takiej jak adres środowiska lub token, ustawianie cichej wartości domyślnej jest zazwyczaj ryzykowne — test może ruszyć na niewłaściwym środowisku.
Dlaczego nie stosować samego except:?
Można napisać:
try:
timeout_seconds = float(config["timeout_seconds"])
except:
timeout_seconds = 10.0
Jednak to zła praktyka. Taki blok przechwytuje niemal wszystko, również problemy, których nie planowałeś obsługiwać: błąd w logice, literówkę w kodzie czy nawet wyjątki związane z przerwaniem działania programu.
W efekcie program może pozornie działać dalej, lecz z ukrytą wadą. W testach automatycznych prowadzi to do szczególnie groźnych wyników: test może zakończyć się sukcesem, chociaż jego przygotowanie albo walidacja nie wykonały się poprawnie.
Stosuj prostą zasadę:
- Przewidź konkretną sytuację, która może się wydarzyć.
- Zidentyfikuj właściwy typ wyjątku.
- Obsłuż go tylko wtedy, gdy wiesz, co program ma zrobić dalej.
- Pozostałe błędy pozostaw widoczne.
Czasem na granicy aplikacji, na przykład w narzędziu uruchamianym z terminala, spotkasz except Exception as error:. Jest to szersze, ale nadal bezpieczniejsze niż gołe except:, ponieważ dotyczy standardowych wyjątków aplikacyjnych. Mimo to nie powinno zastępować konkretnych obsług w zwykłej logice programu.
Warto też zachować informację o samym błędzie:
try:
timeout_seconds = float(raw_timeout)
except ValueError as error:
print(f"Niepoprawny limit czasu: {error}")
Zmienna error zawiera obiekt wyjątku. Jego opis pomaga w diagnostyce, zwłaszcza gdy dane pochodzą z pliku konfiguracji, zmiennych środowiskowych lub odpowiedzi usługi.
Wąski blok try daje lepszą diagnozę
Blok try powinien obejmować tylko operację, która może rzucić spodziewany wyjątek. Dzięki temu nie przechwycisz przypadkiem innego błędu i nie przypiszesz mu błędnej przyczyny.
Porównaj dwa warianty.
try:
raw_timeout = config["timeout_seconds"]
timeout_seconds = float(raw_timeout)
client = ApiClient(base_url, timeout_seconds=timeout_seconds)
except ValueError:
print("Konfiguracja limitu czasu jest błędna.")
Ten kod jest zbyt szeroki. ValueError może powstać zarówno podczas konwersji float(), jak i wewnątrz konstruktora ApiClient. Komunikat obwiniałby konfigurację limitu czasu, nawet gdy faktyczny problem dotyczyłby czegoś innego.
Czytelniejszy wariant rozdziela odpowiedzialności:
raw_timeout = config["timeout_seconds"]
try:
timeout_seconds = float(raw_timeout)
except ValueError as error:
raise ValueError(
"Zmienna TIMEOUT_SECONDS musi zawierać liczbę."
) from error
client = ApiClient(
base_url=base_url,
timeout_seconds=timeout_seconds,
)
Teraz try chroni wyłącznie konwersję tekstu na liczbę. Jeśli konwersja się nie uda, podnosimy nowy ValueError z komunikatem zrozumiałym dla osoby konfigurującej testy.
Zapis from error zachowuje pierwotną przyczynę wyjątku w tracebacku. Dzięki temu w raporcie nadal zobaczysz, że źródłem problemu była funkcja float(), a jednocześnie otrzymasz opis dostosowany do kontekstu projektu.
else i finally: co po obsłudze błędu?
Pełna konstrukcja może zawierać cztery bloki:
try:
# kod, który może zgłosić wyjątek
except SomeError:
# reakcja na konkretny wyjątek
else:
# kod wykonywany tylko wtedy, gdy try się powiódł
finally:
# sprzątanie wykonywane niezależnie od sukcesu lub obsłużonego błędu

else: kod wyłącznie po sukcesie
Blok else jest dobrym miejscem na pracę, która ma sens tylko wtedy, gdy operacja w try się powiodła.
def print_page_number(raw_page: str) -> None:
try:
page_number = int(raw_page)
except ValueError:
print("Numer strony musi być liczbą całkowitą.")
else:
print(f"Pobieranie strony numer {page_number}.")
Jeżeli raw_page ma wartość "trzy", wykona się except, a else zostanie pominięty. Jeśli wartość to "3", except zostanie pominięty, a wykona się else.
Użycie else pomaga utrzymać wąski blok try: ryzykowna konwersja jest w try, a normalna logika po udanej konwersji trafia poza niego.
finally: sprzątanie zasobu
finally wykonuje się po try oraz ewentualnym except, zarówno gdy operacja się udała, jak i gdy doszło do obsłużonego wyjątku. Jest przeznaczony dla czynności porządkowych: zamykania połączenia, usuwania pliku tymczasowego czy zwalniania blokady.
connection = None
try:
connection = open_database_connection()
connection.execute("SELECT 1")
except DatabaseError as error:
print(f"Nie udało się wykonać zapytania: {error}")
finally:
if connection is not None:
connection.close()
W tym przykładzie zamknięcie połączenia jest konieczne niezależnie od wyniku zapytania. Później poznasz wygodniejsze konstrukcje do zarządzania zasobami, ale warto już teraz rozumieć podstawową rolę finally.
Nie umieszczaj w finally logiki, która może zmienić rezultat testu albo ukryć pierwotny problem. Ten blok ma przede wszystkim doprowadzić środowisko do uporządkowanego stanu.
raise: zgłaszanie błędu, gdy dane łamią regułę
Wyjątki można nie tylko przechwytywać. Możesz też zgłaszać je samodzielnie przez raise, gdy wykryjesz, że dane nie spełniają wymagań.
Załóżmy, że funkcja przygotowująca dane testowe wymaga dodatniego identyfikatora użytkownika:
def build_user_url(base_url: str, user_id: int) -> str:
if user_id <= 0:
raise ValueError("user_id musi być dodatnią liczbą całkowitą.")
return f"{base_url.rstrip('/')}/users/{user_id}"
Użycie:
url = build_user_url(
base_url="https://api.example.test",
user_id=42,
)
invalid_url = build_user_url(
base_url="https://api.example.test",
user_id=0,
)
Pierwsze wywołanie zwraca adres. Drugie przerywa wykonanie, ponieważ 0 nie jest prawidłowym identyfikatorem według reguły funkcji.
W pomocniczym kodzie testowym taki błąd powinien zwykle być widoczny, a nie zamieniony na print(). Dzięki temu pytest w przyszłości pokaże nieudany test oraz traceback prowadzący do niewłaściwych danych.
Obejrzyj teraz krótką część materiału o raise. Skup się na tym, że wyjątek może być elementem świadomie zaprojektowanego kontraktu funkcji, a nie wyłącznie reakcją Pythona na nieudaną operację.
13. Kurs Python 3 - rzucanie wyjątków (raise, assert)
Film „13. Kurs Python 3 - rzucanie wyjątków (raise, assert)” z kanału Kanał o Wszystkim przedstawia ręczne zgłaszanie wyjątków oraz ich przechwytywanie.
Obejrzyj raise i komunikaty, aby zobaczyć podstawową składnię ręcznego zgłaszania wyjątku. Potem obejrzyj przechwytywanie raise i prześledź, jak zgłoszony wyjątek może zostać obsłużony albo przekazany wyżej.
Warto odróżnić raise od assert. raise ValueError(...) wyraża świadomą regułę działania funkcji i ma pozostać aktywne w normalnie uruchamianym programie. assert służy głównie do sprawdzania założeń programisty i w testach będzie pełnił inną, bardzo ważną rolę — wrócimy do niego w module o pytest.
Własny typ wyjątku: kiedy ValueError przestaje wystarczać?
Wbudowane wyjątki pokrywają większość prostych przypadków:
ValueErrordla poprawnego typu, ale nieprawidłowej wartości;KeyErrordla brakującego klucza;TypeErrordla niezgodnych typów argumentów;FileNotFoundErrordla nieistniejącego pliku.
Czasem jednak własna nazwa lepiej opisuje problem domenowy. Na przykład dane przeznaczone do testu mogą być nieprawidłowe niezależnie od tego, czy brakuje pola, czy wartość jest błędna:
class TestDataError(Exception):
"""Dane wejściowe scenariusza testowego są niepoprawne."""
Możesz później użyć tego typu:
def require_email(payload: dict[str, str]) -> str:
email = payload.get("email")
if not email:
raise TestDataError(
"Dane testowe muszą zawierać niepusty adres email."
)
return email
Na tym etapie wystarczy znać wzorzec: własny wyjątek dziedziczy po Exception, a jego nazwa kończy się zwykle na Error. Twórz go wtedy, gdy pozwala osobie czytającej kod wyraźnie rozpoznać rodzaj problemu. Dla jednej prostej walidacji ValueError zwykle jest wystarczający.
Decyzja: obsłużyć, zgłosić czy pozwolić przerwać działanie?
W automatyzacji testów nie każdy wyjątek traktuj tak samo.
| Sytuacja | Zalecana reakcja |
|---|---|
| Opcjonalne pole odpowiedzi API nie istnieje | Obsłuż brak tylko wtedy, gdy jest rzeczywiście dozwolony. |
| Tekst konfiguracji nie jest liczbą | Zgłoś czytelny ValueError; test nie powinien działać z przypadkową wartością. |
| Dane testowe łamią ustaloną regułę | Użyj raise, aby zatrzymać scenariusz blisko źródła problemu. |
| Połączenie lub plik trzeba zamknąć | Użyj mechanizmu sprzątania, na przykład finally. |
| Wystąpił nieznany błąd w Twoim kodzie | Nie ukrywaj go szerokim except; traceback jest potrzebny do naprawy. |
Najważniejsza zasada brzmi: przechwytuj wyjątek tylko wtedy, gdy potrafisz podjąć sensowną decyzję po jego wystąpieniu. Sensowną decyzją może być wartość zastępcza, ponowienie operacji, sprzątanie zasobu albo zgłoszenie nowego, bardziej kontekstowego błędu. Samo wypisanie „wystąpił błąd” rzadko jest wystarczające.
Podsumujmy:
- wyjątek przerywa zwykły przebieg programu, chyba że zostanie obsłużony;
tryobejmuje możliwie mały fragment kodu, który może zgłosić przewidywalny wyjątek;exceptpowinien wskazywać konkretne typy błędów, a nie przechwytywać wszystko;elsewykonuje kod wyłącznie po sukcesie blokutry;finallysłuży do sprzątania zasobów niezależnie od wyniku operacji;raisepozwala jawnie egzekwować wymagania funkcji i przekazywać czytelne informacje o niepoprawnych danych;- w testach nie ukrywaj błędów konfiguracji ani wad aplikacji — dobra diagnostyka jest ważniejsza niż pozornie zielony wynik.
W następnej lekcji wykorzystasz słowniki i wyjątki praktycznie: wczytasz konfigurację testów z pliku JSON oraz zmiennych środowiskowych, a błędne lub brakujące wartości obsłużysz w sposób jasny i bezpieczny.
Can't find a good explanation? Sign up and we'll make it for you
Sign up