8. Fehler und Ausnahmen

Bisher wurden Fehlermeldungen nur kurz erwähnt, aber wenn Sie die Beispiele ausprobiert haben, haben Sie wahrscheinlich einige gesehen. Es gibt (mindestens) zwei unterscheidbare Arten von Fehlern: Syntaxfehler und Ausnahmen.

8.1. Syntaxfehler

Syntaxfehler, auch Parsing-Fehler genannt, sind vielleicht die häufigste Art von Beschwerden, die Sie erhalten, während Sie noch Python lernen

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
               ^^^^^
SyntaxError: invalid syntax

Der Parser wiederholt die fehlerhafte Zeile und zeigt kleine Pfeile an, die auf die Stelle zeigen, an der der Fehler erkannt wurde. Beachten Sie, dass dies nicht immer die Stelle ist, die behoben werden muss. Im Beispiel wird der Fehler bei der Funktion print() erkannt, da direkt davor ein Doppelpunkt (':') fehlt.

Der Dateiname (<stdin> in unserem Beispiel) und die Zeilennummer werden ausgegeben, damit Sie wissen, wo Sie suchen müssen, falls die Eingabe aus einer Datei stammt.

8.2. Ausnahmen

Selbst wenn eine Anweisung oder ein Ausdruck syntaktisch korrekt ist, kann sie beim Versuch, sie auszuführen, einen Fehler verursachen. Fehler, die während der Ausführung erkannt werden, werden Ausnahmen genannt und sind nicht unbedingt fatal: Sie werden bald lernen, wie Sie sie in Python-Programmen behandeln können. Die meisten Ausnahmen werden jedoch nicht von Programmen behandelt und führen zu Fehlermeldungen wie hier gezeigt

>>> 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

Die letzte Zeile der Fehlermeldung gibt an, was passiert ist. Ausnahmen gibt es in verschiedenen Typen, und der Typ wird als Teil der Meldung ausgegeben: Die Typen im Beispiel sind ZeroDivisionError, NameError und TypeError. Der als Ausnahme-Typ ausgegebene String ist der Name der aufgetretenen integrierten Ausnahme. Dies gilt für alle integrierten Ausnahmen, muss aber nicht für benutzerdefinierte Ausnahmen gelten (obwohl es eine nützliche Konvention ist). Standard-Ausnahmenamen sind integrierte Bezeichner (keine reservierten Schlüsselwörter).

Der Rest der Zeile liefert Details, die auf dem Typ der Ausnahme und ihrer Ursache basieren.

Der vorhergehende Teil der Fehlermeldung zeigt den Kontext, in dem die Ausnahme aufgetreten ist, in Form eines Stack-Tracebacks. Im Allgemeinen enthält er eine Liste von Quellzeilen; Zeilen, die aus der Standardeingabe gelesen wurden, werden jedoch nicht angezeigt.

Integrierte Ausnahmen listet die integrierten Ausnahmen und ihre Bedeutungen auf.

8.3. Ausnahmen behandeln

Es ist möglich, Programme zu schreiben, die ausgewählte Ausnahmen behandeln. Betrachten Sie das folgende Beispiel, das den Benutzer zur Eingabe auffordert, bis eine gültige Ganzzahl eingegeben wurde, dem Benutzer aber erlaubt, das Programm zu unterbrechen (mit Control-C oder was auch immer das Betriebssystem unterstützt); beachten Sie, dass eine vom Benutzer ausgelöste Unterbrechung durch Auslösen der KeyboardInterrupt-Ausnahme signalisiert wird.

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

Die try-Anweisung funktioniert wie folgt.

  • Zuerst wird die try-Klausel (die Anweisung(en) zwischen den Schlüsselwörtern try und except) ausgeführt.

  • Wenn keine Ausnahme auftritt, wird die except-Klausel übersprungen und die Ausführung der try-Anweisung beendet.

  • Wenn während der Ausführung der try-Klausel eine Ausnahme auftritt, wird der Rest der Klausel übersprungen. Wenn dann ihr Typ mit der Ausnahme übereinstimmt, die nach dem Schlüsselwort except benannt ist, wird die except-Klausel ausgeführt, und dann wird die Ausführung nach dem try/except-Block fortgesetzt.

  • Wenn eine Ausnahme auftritt, die nicht mit der in der except-Klausel genannten Ausnahme übereinstimmt, wird sie an äußere try-Anweisungen weitergegeben; wenn kein Handler gefunden wird, handelt es sich um eine unbehandelte Ausnahme und die Ausführung stoppt mit einer Fehlermeldung.

Eine try-Anweisung kann mehr als eine except-Klausel haben, um Handler für verschiedene Ausnahmen anzugeben. Höchstens ein Handler wird ausgeführt. Handler behandeln nur Ausnahmen, die in der entsprechenden try-Klausel auftreten, nicht in anderen Handlern derselben try-Anweisung. Eine except-Klausel kann mehrere Ausnahmen als tupel in Klammern benennen, zum Beispiel

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

Eine Klasse in einer except-Klausel stimmt mit Ausnahmen überein, die Instanzen der Klasse selbst oder einer ihrer abgeleiteten Klassen sind (aber nicht umgekehrt — eine except-Klausel, die eine abgeleitete Klasse auflistet, stimmt nicht mit Instanzen ihrer Basisklassen überein). Zum Beispiel gibt der folgende Code B, C, D in dieser Reihenfolge aus

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")

Beachten Sie, dass, wenn die except-Klauseln umgekehrt wären (mit except B zuerst), sie B, B, B ausgeben würden — die erste übereinstimmende except-Klausel wird ausgelöst.

Wenn eine Ausnahme auftritt, können damit verbundene Werte vorhanden sein, die auch als Argumente der Ausnahme bezeichnet werden. Die Anwesenheit und die Typen der Argumente hängen vom Ausnahmetyp ab.

Die except-Klausel kann eine Variable nach dem Ausnahmeanamen angeben. Die Variable wird an die Ausnahmeinstanz gebunden, die typischerweise ein args-Attribut hat, das die Argumente speichert. Zur Vereinfachung definieren integrierte Ausnahmetypen __str__(), um alle Argumente zu drucken, ohne explizit auf .args zuzugreifen.

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception type
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

Die Ausgabe von __str__() der Ausnahme wird als letzter Teil („Detail“) der Meldung für unbehandelte Ausnahmen ausgegeben.

BaseException ist die gemeinsame Basisklasse aller Ausnahmen. Eine ihrer Unterklassen, Exception, ist die Basisklasse aller nicht-fatalen Ausnahmen. Ausnahmen, die keine Unterklassen von Exception sind, werden normalerweise nicht behandelt, da sie anzeigen, dass das Programm beendet werden soll. Dazu gehören SystemExit, die von sys.exit() ausgelöst wird, und KeyboardInterrupt, die ausgelöst wird, wenn ein Benutzer das Programm unterbrechen möchte.

Exception kann als Wildcard verwendet werden, die (fast) alles abfängt. Es ist jedoch gute Praxis, so spezifisch wie möglich bei den Arten von Ausnahmen zu sein, die wir behandeln wollen, und unerwartete Ausnahmen weiterpropagieren zu lassen.

Das häufigste Muster für die Behandlung von Exception ist, die Ausnahme zu drucken oder zu protokollieren und sie dann erneut auszulösen (wodurch ein Aufrufer die Ausnahme ebenfalls behandeln kann)

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error:", err)
except ValueError:
    print("Could not convert data to an integer.")
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

Die tryexcept-Anweisung hat eine optionale else-Klausel, die, wenn sie vorhanden ist, allen except-Klauseln folgen muss. Sie ist nützlich für Code, der ausgeführt werden muss, wenn die try-Klausel keine Ausnahme auslöst. Zum Beispiel

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

Die Verwendung der else-Klausel ist besser, als zusätzlichen Code zur try-Klausel hinzuzufügen, da sie versehentliches Abfangen einer Ausnahme vermeidet, die nicht vom Code ausgelöst wurde, der von der tryexcept-Anweisung geschützt wird.

Ausnahmehandler behandeln nicht nur Ausnahmen, die sofort in der try-Klausel auftreten, sondern auch solche, die in Funktionen auftreten, die (auch indirekt) in der try-Klausel aufgerufen werden. Zum Beispiel

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Handling run-time error:', err)
...
Handling run-time error: division by zero

8.4. Ausnahmen auslösen

Die raise-Anweisung erlaubt es dem Programmierer, eine angegebene Ausnahme zu erzwingen. Zum Beispiel

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    raise NameError('HiThere')
NameError: HiThere

Das einzige Argument für raise gibt die auszulösende Ausnahme an. Dies muss entweder eine Ausnahmeinstanz oder eine Ausnahmeklasse sein (eine Klasse, die von BaseException abgeleitet ist, wie z. B. Exception oder eine ihrer Unterklassen). Wenn eine Ausnahmeklasse übergeben wird, wird sie implizit durch Aufruf ihres Konstruktors ohne Argumente instanziiert

raise ValueError  # shorthand for 'raise ValueError()'

Wenn Sie feststellen müssen, ob eine Ausnahme ausgelöst wurde, aber nicht beabsichtigen, sie zu behandeln, erlaubt eine einfachere Form der raise-Anweisung, die Ausnahme erneut auszulösen

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise NameError('HiThere')
NameError: HiThere

8.5. Ausnahmekettenbildung

Wenn innerhalb eines except-Blocks eine unbehandelte Ausnahme auftritt, wird die behandelte Ausnahme daran angehängt und in der Fehlermeldung enthalten sein

>>> 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

Um anzuzeigen, dass eine Ausnahme eine direkte Folge einer anderen ist, erlaubt die raise-Anweisung eine optionale from-Klausel

# exc must be exception instance or None.
raise RuntimeError from exc

Dies kann nützlich sein, wenn Sie Ausnahmen transformieren. Zum Beispiel

>>> 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

Es erlaubt auch, die automatische Ausnahmekettenbildung mit dem from None-Idiom zu deaktivieren

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

Weitere Informationen zu den Mechaniken der Kettenbildung finden Sie unter Integrierte Ausnahmen.

8.6. Benutzerdefinierte Ausnahmen

Programme können ihre eigenen Ausnahmen benennen, indem sie eine neue Ausnahmeklasse erstellen (siehe Klassen für mehr über Python-Klassen). Ausnahmen sollten typischerweise von der Exception-Klasse abgeleitet sein, entweder direkt oder indirekt.

Ausnahmeklassen können so definiert werden, dass sie alles tun können, was jede andere Klasse auch kann, aber sie werden normalerweise einfach gehalten, oft bieten sie nur eine Anzahl von Attributen, die es ermöglichen, Informationen über den Fehler von Handlern für die Ausnahme zu extrahieren.

Die meisten Ausnahmen werden mit Namen definiert, die auf „Error“ enden, ähnlich der Benennung der Standardausnahmen.

Viele Standardmodule definieren ihre eigenen Ausnahmen, um Fehler zu melden, die in den von ihnen definierten Funktionen auftreten können.

8.7. Bereinigungsaktionen definieren

Die try-Anweisung hat eine weitere optionale Klausel, die dazu dient, Bereinigungsaktionen zu definieren, die unter allen Umständen ausgeführt werden müssen. Zum Beispiel

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

Wenn eine finally-Klausel vorhanden ist, wird die finally-Klausel als letzte Aufgabe ausgeführt, bevor die try-Anweisung abgeschlossen ist. Die finally-Klausel wird ausgeführt, unabhängig davon, ob die try-Anweisung eine Ausnahme auslöst oder nicht. Die folgenden Punkte diskutieren komplexere Fälle, wenn eine Ausnahme auftritt

  • Wenn während der Ausführung der try-Klausel eine Ausnahme auftritt, kann die Ausnahme von einer except-Klausel behandelt werden. Wenn die Ausnahme nicht von einer except-Klausel behandelt wird, wird die Ausnahme nach der Ausführung der finally-Klausel erneut ausgelöst.

  • Eine Ausnahme kann während der Ausführung einer except- oder else-Klausel auftreten. Auch hier wird die Ausnahme nach der Ausführung der finally-Klausel erneut ausgelöst.

  • Wenn die finally-Klausel eine break-, continue- oder return-Anweisung ausführt, werden Ausnahmen nicht erneut ausgelöst. Dies kann verwirrend sein und wird daher nicht empfohlen. Ab Version 3.14 gibt der Compiler dafür eine SyntaxWarning aus (siehe PEP 765).

  • Wenn die try-Anweisung eine break-, continue- oder return-Anweisung erreicht, wird die finally-Klausel ausgeführt, unmittelbar bevor die break-, continue- oder return-Anweisung ausgeführt wird.

  • Wenn eine finally-Klausel eine return-Anweisung enthält, ist der zurückgegebene Wert der von der return-Anweisung der finally-Klausel, nicht der Wert aus der return-Anweisung der try-Klausel. Dies kann verwirrend sein und wird daher nicht empfohlen. Ab Version 3.14 gibt der Compiler dafür eine SyntaxWarning aus (siehe PEP 765).

Zum Beispiel

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

Ein komplizierteres Beispiel

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
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'

Wie Sie sehen, wird die finally-Klausel auf jeden Fall ausgeführt. Die von der Division zweier Strings ausgelöste TypeError wird von der except-Klausel nicht behandelt und daher nach der Ausführung der finally-Klausel erneut ausgelöst.

In realen Anwendungen ist die finally-Klausel nützlich, um externe Ressourcen (wie Dateien oder Netzwerkverbindungen) freizugeben, unabhängig davon, ob die Verwendung der Ressource erfolgreich war.

8.8. Vordefinierte Bereinigungsaktionen

Einige Objekte definieren Standard-Bereinigungsaktionen, die durchgeführt werden müssen, wenn das Objekt nicht mehr benötigt wird, unabhängig davon, ob die Operation mit dem Objekt erfolgreich war oder fehlgeschlagen ist. Betrachten Sie das folgende Beispiel, das versucht, eine Datei zu öffnen und ihren Inhalt auf dem Bildschirm auszugeben.

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

Das Problem mit diesem Code ist, dass die Datei für eine unbestimmte Zeit offen bleibt, nachdem dieser Teil des Codes seine Ausführung beendet hat. Dies ist in einfachen Skripten kein Problem, kann aber für größere Anwendungen ein Problem darstellen. Die with-Anweisung ermöglicht es, Objekte wie Dateien so zu verwenden, dass sichergestellt ist, dass sie immer zeitnah und korrekt bereinigt werden.

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

Nachdem die Anweisung ausgeführt wurde, wird die Datei f immer geschlossen, selbst wenn bei der Verarbeitung der Zeilen ein Problem aufgetreten ist. Objekte, die wie Dateien vordefinierte Bereinigungsaktionen bieten, weisen dies in ihrer Dokumentation aus.

8.9. Mehrere unabhängige Ausnahmen auslösen und behandeln

Es gibt Situationen, in denen es notwendig ist, mehrere aufgetretene Ausnahmen zu melden. Dies ist oft in Nebenläufigkeits-Frameworks der Fall, wenn mehrere Aufgaben parallel fehlgeschlagen sind, es gibt aber auch andere Anwendungsfälle, in denen es wünschenswert ist, die Ausführung fortzusetzen und mehrere Fehler zu sammeln, anstatt die erste Ausnahme auszulösen.

Die integrierte ExceptionGroup kapselt eine Liste von Ausnahmeinstanzen, sodass sie zusammen ausgelöst werden können. Sie ist selbst eine Ausnahme, kann also wie jede andere Ausnahme abgefangen werden.

>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 3, in f
  |     raise ExceptionGroup('there were problems', excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>

Durch die Verwendung von except* anstelle von except können wir selektiv nur die Ausnahmen in der Gruppe behandeln, die einem bestimmten Typ entsprechen. Im folgenden Beispiel, das eine verschachtelte Ausnahmegruppe zeigt, extrahiert jede except*-Klausel Ausnahmen eines bestimmten Typs aus der Gruppe, während alle anderen Ausnahmen zu anderen Klauseln weiterpropagieren und schließlich erneut ausgelöst werden.

>>> def f():
...     raise ExceptionGroup(
...         "group1",
...         [
...             OSError(1),
...             SystemError(2),
...             ExceptionGroup(
...                 "group2",
...                 [
...                     OSError(3),
...                     RecursionError(4)
...                 ]
...             )
...         ]
...     )
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were 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: group1 (1 sub-exception)
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2 (1 sub-exception)
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------
>>>

Beachten Sie, dass die in einer Ausnahmegruppe verschachtelten Ausnahmen Instanzen und keine Typen sein müssen. Dies liegt daran, dass in der Praxis die Ausnahmen typischerweise solche wären, die bereits vom Programm ausgelöst und abgefangen wurden, nach dem folgenden Muster

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

8.10. Ausnahmen mit Notizen anreichern

Wenn eine Ausnahme erstellt wird, um ausgelöst zu werden, wird sie normalerweise mit Informationen initialisiert, die den aufgetretenen Fehler beschreiben. Es gibt Fälle, in denen es nützlich ist, Informationen hinzuzufügen, nachdem die Ausnahme abgefangen wurde. Zu diesem Zweck haben Ausnahmen eine Methode add_note(note), die einen String akzeptiert und ihn zur Notizenliste der Ausnahme hinzufügt. Die standardmäßige Traceback-Darstellung enthält alle Notizen in der Reihenfolge, in der sie hinzugefügt wurden, nach der Ausnahme.

>>> try:
...     raise TypeError('bad type')
... except Exception as e:
...     e.add_note('Add some information')
...     e.add_note('Add some more information')
...     raise
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>

Zum Beispiel können wir, wenn wir Ausnahmen in einer Ausnahmegruppe sammeln, Kontextinformationen für die einzelnen Fehler hinzufügen. Im Folgenden hat jede Ausnahme in der Gruppe eine Notiz, die angibt, wann dieser Fehler aufgetreten ist.

>>> def f():
...     raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
...     try:
...         f()
...     except Exception as e:
...         e.add_note(f'Happened in Iteration {i+1}')
...         excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     raise ExceptionGroup('We have some problems', excs)
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>