# 8. Błędy i wyjątki

Do tej pory wiadomości o błędach były tylko wspomniane, ale jeśli próbowałaś(-łeś) przykładów to pewnie udało ci się na nie natknąć. Występują (przynajmniej) dwa charakterystyczne typy błędów: błędy składni (syntax errors) oraz wyjątki (exceptions).

## 8.1. Błędy składni

Błędy składni, znane również jako błędy parsowania, są prawdopodobnie najczęstszym rodzajem skarg, które pojawiają się podczas nauki Pythona:

    >>> while True print('Witaj świecie')
      File "<stdin>", line 1
        while True print('Witaj świecie')
                   ^^^^^
    SyntaxError: invalid syntax

Parser powtarza błędną linię i wyświetla małe strzałki wskazujące miejsce, w którym wykryto błąd. Należy pamiętać, że nie zawsze jest to miejsce, które wymaga naprawy. W przykładzie błąd jest wykryty na funkcji `print()`, ponieważ brakuje tuż przed nią dwukropka (`':'`).

Nazwa pliku (`<stdin>` w naszym przykładzie) i numer linii są drukowane, abyś wiedział(a), gdzie szukać, w przypadku, gdy dane wejściowe pochodzą z pliku.

## 8.2. Wyjątki

Nawet jeśli instrukcja lub wyrażenie jest poprawne składniowo, może ona wywołać błąd podczas próby jej wykonania. Błędy zauważone podczas wykonania programu są nazywane wyjątkami (exceptions) i nie zawsze są niedopuszczalne: już niedługo nauczysz w jaki sposób je obsługiwać. Większość wyjątków nie jest jednak obsługiwana przez program przez co wyświetlane są informacje o błędzie jak pokazano poniżej:

    >>> 10 * (1/0)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        10 * (1/0)
              ~^~
    ZeroDivisionError: division by zero
    >>> 4 + spam*3
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        4 + spam*3
            ^^^^
    NameError: name 'spam' is not defined
    >>> '2' + 2
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        '2' + 2
        ~~~~^~~
    TypeError: can only concatenate str (not "int") to str

Ostatni wiersz komunikatu o błędzie wskazuje, co się stało. Wyjątki występują w różnych typach, a typ jest drukowany jako część komunikatu: typy w przykładzie to `ZeroDivisionError`, `NameError` i `TypeError`. Ciąg wydrukowany jako typ wyjątku jest nazwą wbudowanego wyjątku, który wystąpił. Jest to prawdą dla wszystkich wbudowanych wyjątków, ale nie musi być prawdą dla wyjątków zdefiniowanych przez użytkownika (choć jest to przydatna konwencja). Standardowe nazwy wyjątków są wbudowanymi identyfikatorami (nie zarezerwowanymi słowami kluczowymi).

Pozostała część linii dostarcza szczegółów na temat typu wyjątku oraz informacji, co go spowodowało.

Wcześniejsza część komunikatu o błędzie pokazuje kontekst, w którym wystąpił wyjątek, w postaci śladu stosu. Ogólnie rzecz biorąc, zawiera on ślad stosu z listą linii źródłowych; jednak nie wyświetli linii odczytanych ze standardowego wejścia.

Built-in Exceptions wymienia wbudowane wyjątki i ich znaczenie.

## 8.3. Obsługa wyjątków

Możliwe jest pisanie programów, które obsługują wybrane wyjątki. Spójrzmy na poniższy przykład, który prosi użytkownika o wprowadzenie danych, dopóki nie zostanie wprowadzona poprawna liczba całkowita, ale pozwala użytkownikowi na przerwanie programu (przy użyciu `Control`-`C` lub czegokolwiek innego obsługiwanego przez system operacyjny); zauważ, że przerwanie wygenerowane przez użytkownika jest sygnalizowane przez podniesienie wyjątku `KeyboardInterrupt`.

    >>> while True:
    ...     try:
    ...         x = int(input("Proszę podaj liczbę: "))
    ...         break
    ...     except ValueError:
    ...         print("Ups! To nie była poprawna liczba. Spróbuj ponownie...")
    ...

Instrukcja `try` działa następująco.

- W pierwszej kolejności wykonywane są instrukcje pod klauzulą try - pomiędzy słowami kluczowymi `try` i `except`.

- Jeżeli nie wystąpi żaden wyjątek, klauzula except jest pomijana i zostaje zakończone wykonywanie instrukcji `try`.

- Jeśli wyjątek wystąpi podczas wykonywania klauzuli `try`, reszta klauzuli jest pomijana. Następnie, jeśli jego typ pasuje do wyjątku nazwanego po słowie kluczowym `except`, wykonywana jest klauzula except, a następnie wykonanie jest kontynuowane po bloku try/except.

- Jeśli wystąpi wyjątek, który nie pasuje do wyjątku nazwanego w klauzuli except, jest on przekazywany do zewnętrznych instrukcji `try`; jeśli nie zostanie znaleziona obsługa, jest to nieobsłużony wyjątek i wykonanie zatrzymuje się z komunikatem błędu.

Instrukcja `try` może mieć więcej niż jedną klauzulę except, aby określić programy obsługi dla różnych wyjątków. Wykonany zostanie co najwyżej jeden handler. Obsługiwane są tylko wyjątki, które występują w odpowiadających im klauzulach try, a nie w kodzie obsługi tej samej instrukcji `try`. Klauzula except może określać wiele wyjątków, na przykład:

    ... except RuntimeError, TypeError, NameError:
    ...     pass

Klasa w klauzuli `except` obsługuje wyjątki, które są instancjami samej klasy lub jednej z jej klas pochodnych (ale nie na odwrót — klauzula except wymieniająca klasę pochodną nie obsłuży instancji jej klas bazowych). Na przykład, poniższy kod wypisze B, C, D w tej kolejności:

    class B(Exception):
        pass

    class C(B):
        pass

    class D(C):
        pass

    for cls in [B, C, D]:
        try:
            raise cls()
        except D:
            print("D")
        except C:
            print("C")
        except B:
            print("B")

Zauważ, że jeśli klauzule except byłyby odwrócone (z `except B` na pierwszym miejscu), wypisane zostałoby B, B, B — uruchamiana jest pierwsza pasująca klauzula except.

Gdy wystąpi wyjątek, może on mieć powiązane wartości, znane również jako argumenty wyjątku. Obecność i typy argumentów zależą od typu wyjątku.

Klauzula except może określać nazwę zmiennej po nazwie wyjątku. Zmienna jest powiązana z instancją wyjątku, która zazwyczaj posiada atrybut `args` przechowujący argumenty. Dla wygody, typy wyjątków wbudowanych definiują `__str__()` drukującą wszystkie argumenty bez odwoływania się do `.args`.

    >>> try:
    ...     raise Exception('konserwa', 'jajka')
    ... except Exception as inst:
    ...     print(type(inst))    # typ wyjątku
    ...     print(inst.args)     # argumenty przechowywane w .args
    ...     print(inst)          # metoda __str__ pozwala na wypisanie args bezpośrednio,
    ...                          # ale jej definicja może być nadpisana w klasach dziedziczących
    ...     x, y = inst.args     # rozpakowanie argumentów
    ...     print('x =', x)
    ...     print('y =', y)
    ...
    <class 'Exception'>
    ('konserwa', 'jajka')
    ('konserwa', 'jajka')
    x = konserwa
    y = jajka

Dane wyjściowe metody `__str__()` wyjątku są drukowane jako ostatnia część («szczegóły») komunikatu dla nieobsłużonych wyjątków.

`BaseException` jest wspólną klasą bazową wszystkich wyjątków. Jedna z jej podklas, `Exception`, jest klasą bazową wszystkich nie-fatalnych wyjątków. Wyjątki, które nie są podklasami `Exception` nie są zazwyczaj obsługiwane, ponieważ są używane do wskazania, że program powinien się zakończyć. Obejmują one `SystemExit`, który jest rzucany przez `sys.exit()` i `KeyboardInterrupt`, które są rzucane, gdy użytkownik chce przerwać program.

`Exception` może być używany jako symbol wieloznaczny, który przechwytuje (prawie) wszystko. Dobrą praktyką jest jednak jak najdokładniejsze określenie typów wyjątków, które zamierzamy obsługiwać, i umożliwienie propagacji wszelkim nieoczekiwanym wyjątkom.

Najczęstszym wzorcem obsługi `Exception` jest drukowanie lub umieszczenie wyjątku w logach, a następnie ponowne rzucenie go (umożliwiając również obsługę wyjątku funkcji, w której znajduje się wywołanie):

    import sys

    try:
        f = open('moj_plik.txt')
        s = f.readline()
        i = int(s.strip())
    except OSError as err:
        print("Błąd OS:", err)
    except ValueError:
        print("Nie udało się przekonwertować danych na liczbę całkowitą.")
    except Exception as err:
        print(f"Nieoczekiwany {err=}, {type(err)=}")
        raise

Instrukcja `try` … `except` posiada opcjonalną klauzulę else, która, gdy jest obecna, musi następować po wszystkich klauzulach except. Jest to przydatne w przypadku kodu, który musi zostać wykonany, jeśli klauzula try nie rzuci wyjątku. Na przykład:

    for arg in sys.argv[1:]:
        try:
            f = open(arg, 'r')
        except OSError:
            print('nie mogę otworzyć', arg)
        else:
            print(arg, 'ma', len(f.readlines()), 'linii')
            f.close()

Użycie klauzuli `else` jest lepsze niż dodanie dodatkowego kodu do klauzuli `try`, ponieważ pozwala uniknąć przypadkowego wychwycenia wyjątku, który nie został rzucony przez kod chroniony instrukcją `try`… `except`.

Instrukcja try … catch nie obsługuje tylko wyjątków, które występują bezpośrednio w klauzuli try, ale także te, które występują wewnątrz funkcji, które są wywoływane (nawet pośrednio) w klauzuli try. Na przykład:

    >>> def this_fails():
    ...     x = 1/0
    ...
    >>> try:
    ...     this_fails()
    ... except ZeroDivisionError as err:
    ...     print('Błąd w czasie wykonania:', err)
    ...
    Błąd w czasie wykonania: division by zero

## 8.4. Rzucanie wyjątków

Instrukcja `raise` pozwala programiście wymusić wystąpienie żądanego wyjątku. Na przykład:

    >>> raise NameError('CześćiCzołem')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        raise NameError('CześćiCzołem')
    NameError: CześćiCzołem

Jedyny argument do `raise` wskazuje wyjątek, który ma być rzucony. Musi to być albo instancja wyjątku, albo klasa wyjątku (klasa dziedzicząca z `BaseException`, taka jak `Exception` lub jedna z jej podklas). Jeśli przekazana zostanie klasa wyjątku, zostanie ona niejawnie zainicjowana przez wywołanie jej konstruktora bez argumentów:

    raise ValueError  # skrót dla 'raise ValueError()'

Jeśli chcesz rozpoznać, czy wyjątek został rzucony, ale nie zamierzasz go obsługiwać, prostsza forma instrukcji `raise` pozwala na ponowne rzucenie wyjątku:

    >>> try:
    ...     raise NameError('CześćiCzołem')
    ... except NameError:
    ...     print('Przeleciał wyjątek!')
    ...     raise
    ...
    Przeleciał wyjątek!
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        raise NameError('CześćiCzołem')
    NameError: CześćiCzołem

## 8.5. Łańcuch wyjątków

Jeśli nieobsłużony wyjątek wystąpi wewnątrz sekcji `except`, zostanie do niego dołączony obsługiwany wyjątek i uwzględniony w komunikacie o błędzie:

    >>> try:
    ...     open("database.sqlite")
    ... except OSError:
    ...     raise RuntimeError("unable to handle error")
    ...
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        open("database.sqlite")
        ~~~~^^^^^^^^^^^^^^^^^^^
    FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
        raise RuntimeError("unable to handle error")
    RuntimeError: unable to handle error

Aby wskazać, że wyjątek jest bezpośrednią konsekwencją innego, instrukcja `raise` dopuszcza opcjonalną klauzulę `from`:

    # exc musi być instancją klasy Exception albo mieć wartość None
    raise RuntimeError from exc

Może to być przydatne podczas przekształcania wyjątków. Na przykład:

    >>> def func():
    ...     raise ConnectionError
    ...
    >>> try:
    ...     func()
    ... except ConnectionError as exc:
    ...     raise RuntimeError('Failed to open database') from exc
    ...
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        func()
        ~~~~^^
      File "<stdin>", line 2, in func
    ConnectionError

    The above exception was the direct cause of the following exception:

    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
        raise RuntimeError('Failed to open database') from exc
    RuntimeError: Failed to open database

Umożliwia również wyłączenie automatycznego łączenia wyjątków przy użyciu idiomu `from None`:

    >>> try:
    ...     open('bazadanych.sqlite')
    ... except OSError:
    ...     raise RuntimeError from None
    ...
    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
        raise RuntimeError from None
    RuntimeError

Więcej informacji na temat mechaniki łączenia w łańcuchy można znaleźć w rozdziale Built-in Exceptions.

## 8.6. Wyjątki zdefiniowane przez użytkownika

Programy mogą nazywać własne wyjątki, tworząc nową klasę wyjątków (więcej informacji na temat klas Python można znaleźć w rozdziale Klasy). Wyjątki powinny zazwyczaj dziedziczyć z klasy `Exception`, bezpośrednio lub pośrednio.

Można zdefiniować klasy wyjątków, które robią wszystko, co może zrobić każda inna klasa, ale zwykle są one proste, często oferując tylko kilka atrybutów, które pozwalają na wyodrębnienie informacji o błędzie przez kod obsługi wyjątku.

Większość wyjątków ma nazwy kończące się na „Error”, podobnie jak w przypadku standardowych wyjątków.

Wiele standardowych modułów definiuje własne wyjątki w celu zgłaszania błędów, które mogą wystąpić w zdefiniowanych w nich funkcjach.

## 8.7. Definiowanie działań porządkujących

Instrukcja `try` ma inną opcjonalną klauzulę, która jest przeznaczona do definiowania działań porządkujących, które muszą być wykonane w każdych okolicznościach. Na przykład:

    >>> try:
    ...     raise KeyboardInterrupt
    ... finally:
    ...     print('Do widzenia, świecie!')
    ...
    Do widzenia, świecie!
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        raise KeyboardInterrupt
    KeyboardInterrupt

Jeśli klauzula `finally` jest obecna, klauzula `finally` wykona się jako ostatnie zadanie przed zakończeniem instrukcji `try`. Klauzula `finally` uruchomi się niezależnie od tego, czy instrukcja `try` spowoduje wyjątek. Poniższe punkty omawiają bardziej złożone przypadki wystąpienia wyjątku:

- Jeśli podczas wykonywania klauzuli `try` wystąpi wyjątek, może on zostać obsłużony przez klauzulę `except`. Jeśli wyjątek nie zostanie obsłużony przez klauzulę `except`, zostanie on ponownie rzucony po wykonaniu klauzuli `finally`.

- Wyjątek może wystąpić podczas wykonywania klauzul `except` lub `else`. Ponownie, wyjątek jest ponownie rzucany po wykonaniu klauzuli `finally`.

- Jeśli klauzula `finally` wykonuje `break`, `continue` lub instrukcję `return`, wyjątki nie są dalej rzucane. Może to być mylące i dlatego jest odradzane. Od wersji 3.14 kompilator emituje `SyntaxWarning` dla takich przypadków (patrz PEP 765).

- Jeśli instrukcja `try` osiągnie instrukcję `break`, `continue` lub `return`, klauzula `finally` wykona się tuż przed wykonaniem instrukcji `break`, `continue` lub `return`.

- Jeśli klauzula `finally` zawiera instrukcję `return`, zwróconą wartością będzie ta z instrukcji `return` klauzuli `finally`, a nie wartość z instrukcji `return` klauzuli `try`. Może to być mylące i dlatego jest odradzane. Od wersji 3.14 kompilator emituje `SyntaxWarning` dla takich przypadków (patrz PEP 765).

Na przykład:

    >>> def bool_return():
    ...     try:
    ...         return True
    ...     finally:
    ...         return False
    ...
    >>> bool_return()
    False

Bardziej skomplikowany przykład:

    >>> def divide(x, y):
    ...     try:
    ...         result = x / y
    ...     except ZeroDivisionError:
    ...         print("dzielenie przez zero!")
    ...     else:
    ...         print("wynik to", result)
    ...     finally:
    ...         print("wykonywanie klauzuli finally")
    ...
    >>> divide(2, 1)
    wynik to 2.0
    wykonywanie klauzuli finally
    >>> divide(2, 0)
    dzielenie przez zero!
    wykonywanie klauzuli finally
    >>> divide("2", "1")
    wykonywanie klauzuli finally
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        divide("2", "1")
        ~~~~~~^^^^^^^^^^
      File "<stdin>", line 3, in divide
        result = x / y
                 ~~^~~
    TypeError: unsupported operand type(s) for /: 'str' and 'str'

Jak widać, klauzula `finally` jest wykonywana w każdym przypadku. `TypeError` rzucony przez dzielenie dwóch ciągów znaków nie jest obsłużony przez klauzulę `except` i dlatego jest ponownie rzucony po wykonaniu klauzuli `finally`.

W prawdziwych aplikacjach, klauzula `finally` jest przydatna do zwalniania zewnętrznych zasobów (takich jak pliki lub połączenia sieciowe), niezależnie od tego, czy użycie zasobu zakończyło się powodzeniem.

## 8.8. Predefiniowane akcje porządkujące

Niektóre obiekty definiują standardowe akcje porządkujące, które mają zostać podjęte, gdy obiekt nie jest już potrzebny, niezależnie od tego, czy operacja przy użyciu obiektu powiodła się, czy nie. Spójrz na poniższy przykład, który próbuje otworzyć plik i wydrukować jego zawartość na ekranie:

    for line in open("moj_plik.txt"):
        print(line, end="")

Problem z tym kodem polega na tym, że pozostawia on plik otwarty przez nieokreślony czas po zakończeniu wykonywania tej części kodu. Nie jest to problemem w prostych skryptach, ale może być problemem dla większych aplikacjach. Instrukcja `with` pozwala na używanie obiektów takich jak pliki w sposób, który zapewnia, że są one zawsze czyszczone szybko i poprawnie:

    with open("moj_plik.txt") as f:
        for line in f:
            print(line, end="")

Po wykonaniu instrukcji, plik f jest zawsze zamykany, nawet jeśli napotkano problem podczas przetwarzania linii. Obiekty, które, podobnie jak pliki, zapewniają predefiniowane akcje czyszczenia, wskażą to w swojej dokumentacji.

## 8.9. Rzucanie i obsługa wielu niepowiązanych wyjątków

Istnieją sytuacje, w których konieczne jest zgłoszenie kilku wyjątków, które wystąpiły. Dzieje się tak często w przypadku frameworków współbieżności, gdy kilka zadań może zakończyć się niepowodzeniem równolegle, ale istnieją również inne przypadki użycia, w których pożądane jest kontynuowanie wykonywania i zbieranie wielu błędów zamiast rzucenia pierwszego wyjątku.

Wbudowana `ExceptionGroup` zawiera listę instancji wyjątków, dzięki czemu mogą one być rzucone razem. Sama w sobie jest wyjątkiem, więc może być przechwycona jak każdy inny wyjątek:

    >>> def f():
    ...     excs = [OSError('błąd 1'), SystemError('błąd 2')]
    ...     raise ExceptionGroup('były problemy', excs)
    ...
    >>> f()
      + Exception Group Traceback (most recent call last):
      |   File "<stdin>", line 1, in <module>
      |     f()
      |     ~^^
      |   File "<stdin>", line 3, in f
      |     raise ExceptionGroup('były problemy', excs)
      | ExceptionGroup: były problemy (2 sub-exceptions)
      +-+---------------- 1 ----------------
        | OSError: błąd 1
        +---------------- 2 ----------------
        | SystemError: błąd 2
        +------------------------------------
    >>> try:
    ...     f()
    ... except Exception as e:
    ...     print(f'złapany {type(e)}: {e}')
    ...
    złapany <class 'ExceptionGroup'>: były problemy (2 sub-exceptions)
    >>>

Używając `except*` zamiast `except`, możemy selektywnie obsługiwać tylko te wyjątki w grupie, które pasują do określonego typu. W poniższym przykładzie, który pokazuje zagnieżdżoną grupę wyjątków, każda klauzula `except*` wyodrębnia z grupy wyjątki określonego typu, pozwalając wszystkim innym wyjątkom propagować się do innych klauzul i ostatecznie być ponownie zgłaszanymi.

    >>> def f():
    ...     raise ExceptionGroup(
    ...         "grupa1",
    ...         [
    ...             OSError(1),
    ...             SystemError(2),
    ...             ExceptionGroup(
    ...                 "grupa2",
    ...                 [
    ...                     OSError(3),
    ...                     RecursionError(4)
    ...                 ]
    ...             )
    ...         ]
    ...     )
    ...
    >>> try:
    ...     f()
    ... except* OSError as e:
    ...     print("Były OSErrors")
    ... except* SystemError as e:
    ...     print("Były SystemErrors")
    ...
    Były OSErrors
    Były SystemErrors
      + Exception Group Traceback (most recent call last):
      |   File "<stdin>", line 2, in <module>
      |     f()
      |     ~^^
      |   File "<stdin>", line 2, in f
      |     raise ExceptionGroup(
      |     ...<12 lines>...
      |     )
      | ExceptionGroup: grupa1 (1 sub-exception)
      +-+---------------- 1 ----------------
        | ExceptionGroup: grupa2 (1 sub-exception)
        +-+---------------- 1 ----------------
          | RecursionError: 4
          +------------------------------------
    >>>

Należy pamiętać, że wyjątki zagnieżdżone w grupie wyjątków muszą być instancjami, a nie typami. Wynika to z faktu, że w praktyce wyjątki są zazwyczaj tymi, które zostały już rzucone i przechwycone przez program, zgodnie z następującym wzorcem:

    >>> excs = []
    ... for test in tests:
    ...     try:
    ...         test.run()
    ...     except Exception as e:
    ...         excs.append(e)
    ...
    >>> if excs:
    ...    raise ExceptionGroup("Niepowodzenia testów", excs)
    ...

## 8.10. Wzbogacanie wyjątków o notatki

Gdy wyjątek jest tworzony w celu rzucenia, jest on zwykle inicjowany informacjami opisującymi błąd, który wystąpił. Istnieją przypadki, w których przydatne jest dodanie informacji po przechwyceniu wyjątku. W tym celu wyjątki mają metodę `add_note(note)`, która akceptuje ciąg znaków i dodaje go do listy notatek wyjątku. Standardowe renderowanie tracebacku zawiera wszystkie notatki, w kolejności, w jakiej zostały dodane, po wyjątku.

    >>> try:
    ...     raise TypeError('zły typ')
    ... except Exception as e:
    ...     e.add_note('Dodaj trochę informacji')
    ...     e.add_note('Dodaj jeszcze trochę informacji')
    ...     raise
    ...
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        raise TypeError('zły typ')
    TypeError: zły typ
    Dodaj trochę informacji
    Dodaj jeszcze trochę informacji
    >>>

Na przykład zbierając wyjątki w grupę wyjątków, możemy chcieć dodać informacje kontekstowe dla poszczególnych błędów. Poniżej każdy wyjątek w grupie ma notatkę wskazującą, kiedy wystąpił.

    >>> def f():
    ...     raise OSError('operacja się nie udała')
    ...
    >>> excs = []
    >>> for i in range(3):
    ...     try:
    ...         f()
    ...     except Exception as e:
    ...         e.add_note(f'Zdarzyło się w iteracji {i+1}')
    ...         excs.append(e)
    ...
    >>> raise ExceptionGroup('Mamy jakieś problemy', excs)
      + Exception Group Traceback (most recent call last):
      |   File "<stdin>", line 1, in <module>
      |     raise ExceptionGroup('Mamy jakieś problemy', excs)
      | ExceptionGroup: Mamy jakieś problemy (3 sub-exceptions)
      +-+---------------- 1 ----------------
        | Traceback (most recent call last):
        |   File "<stdin>", line 3, in <module>
        |     f()
        |     ~^^
        |   File "<stdin>", line 2, in f
        |     raise OSError('operacja się nie udała')
        | OSError: operacja się nie udała
        | Zdarzyło się w iteracji 1
        +---------------- 2 ----------------
        | Traceback (most recent call last):
        |   File "<stdin>", line 3, in <module>
        |     f()
        |     ~^^
        |   File "<stdin>", line 2, in f
        |     raise OSError('operacja się nie udała')
        | OSError: operacja się nie udała
        | Zdarzyło się w iteracji 2
        +---------------- 3 ----------------
        | Traceback (most recent call last):
        |   File "<stdin>", line 3, in <module>
        |     f()
        |     ~^^
        |   File "<stdin>", line 2, in f
        |     raise OSError('operacja się nie udała')
        | OSError: operacja się nie udała
        | Zdarzyło się w iteracji 3
        +------------------------------------
    >>>