Python Topics : Errors and Exceptions
Syntax Errors
common error when learning Python
>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
               ^^^^^
SyntaxError: invalid syntax
Exceptions
execution errors
>>> 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
Built-in Exceptions lists the built-in exceptions and their meanings

Handling Exceptions
>>> while True:
...    try:
...        x = int(input("Please enter a number: "))
...        break
...    except ValueError:
...        print("Oops!  That was not a valid number.  Try again...")
a try statement may have more than one except clause specifying handlers for different exceptions
an except clause may name multiple exceptions as a parenthesized tuple
...    except (RuntimeError, TypeError, NameError):
...         pass
class B is derived from Exception
as written the code will print B, C and D in that order
if the order of the except clauses is reversed the output will be B, B, B because the first matching clause is executed
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")
the except clause may specify a variable after the exception name
builtin exception types define __str__() to print all the arguments without explicitly accessing .args
>>> 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
BaseException is the common base class of all exceptions
Exception derived from BaseException is the base class of all the non-fatal exceptions
Exceptions which are not subclasses of Exception are not typically handled
they are used to indicate that the program should terminate most common pattern for handling Exception is to print or log the exception and then re-raise it
let the caller also handle the exception
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
The try ... except statement has an optional else clause
the statement must follow all except clauses
code that must be executed if the try clause does not raise an exception
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()
Raising Exceptions
the raise statement allows the forcing of a specified exception to occur
>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    raise NameError('HiThere')
    NameError: HiThere
to re-raise an exception
>>> 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
Exception Chaining
if an unhandled exception occurs inside an except section, it will have the exception being handled attached to it and included in the error message
>>> 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
the raise statement allows an optional from clause to indicate that an exception is a direct consequence of another exception
# exc must be exception instance or None.
raise RuntimeError from exc
useful when transforming exceptions
>>> 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
Defining Clean-up Actions
finally clause defines clean-up actions that must be executed under all circumstances
>>> try:
...    raise KeyboardInterrupt
... finally:
...    print('Goodbye, world!')

Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise KeyboardInterrupt
KeyboardInterrupt
if a finally clause is present, the finally clause will execute as the last task before the try statement completes
the finally clause runs whether or not the try statement produces an exception
  • if an exception occurs during execution of the try clause, the exception may be handled by an except clause
    if the exception is not handled by an except clause, the exception is re-raised after the finally clause has been executed
  • an exception could occur during execution of an except or else clause
    the exception is re-raised after the finally clause has been executed
  • if the finally clause executes a break, continue or return statement exceptions are not re-raised
  • if the try statement reaches a break, continue or return statement, the finally clause will execute just prior to the statement's execution
  • if a finally clause includes a return statement, the returned value will be the one from the finally clause's return statement not the value from the try clause's return statement.
Predefined Clean-up Actions
some objects have predefined clean-up actions
with open("myfile.txt") as f:
    for line in f:
        print(line, end="")
with keyword closes the file when the block is exited
Raising and Handling Multiple Unrelated Exceptions
situations where it is necessary to report several exceptions that have occurred
often the case in concurrency frameworks when several tasks may have failed in parallel
other use cases where it is desirable to continue execution and collect multiple errors rather than raise the first exception
ExceptionGroup wraps a list of exception instances so that they can be raised together
ExceptionGroup is a subclass of Exception and can be handled like any other exception
>>> 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
    +------------------------------------
>>> f()
...    except Exception as e:
...       print(f'caught {type(e)}: e')

caught <class 'ExceptionGroup'>: e
use except* instead of except to selectively handle only the exceptions in the group that match a certain type
unhandled exceptions in the group propagate to the next handler
>>> 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
        +------------------------------------
exceptions nested in an ExceptionGroup must be instances
pattern for 'collecting' exceptions
>>> excs = []
... for test in tests:
...    try:
...        test.run()
...    except Exception as e:
...        excs.append(e)

>>> if excs:
...   raise ExceptionGroup("Test Failures", excs)
Enriching Exceptions with Notes
it can be useful to add information after an exception was caught
exceptions have a method add_note(note) that accepts a string and adds it to the exception's notes list
standard traceback rendering includes all notes, in the order they were added, after the exception
>>> 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
can also add context information to an exception
>>> 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
    +------------------------------------
index