try ... except ... finally
Statement
- Handle selected exceptions.
- E.g. ask user for input until a valid integer has been entered:
while True: try: num1 = int(input("Please enter a number: ")) break except ValueError: print("Oops! That was no valid integer. Try again...")
- We can access arguments of any exception by bounding the exception to a variable:
except Exception as e: print(e.args)
Though usually you do not need to do this since if it is just for the sake of printing in python all of the built-in exceptions will log the
args
too (more on that when we talked about classes).Just if you are really curious
In the built-in exception classes we have a method called
__str__()
which will be called by Python when it tries to convert a class object to a string. So inside that class Python is returning theargs
as a string. - Or we can handle different cases:
try: # ... result = 1 / 0 except ZeroDivisionError as e: print(e) except Exception as e: # Catches any kind of error print(e) finally: # Cleanup code
Keep in mind that at most one of the
except
blocks will be executed. - You can have multiple
try ... except
blocks inside one another:def divide_numbers(numerator: int, denominator: int) -> float | None: try: return numerator / denominator except ValueError: print("Something wrong with values") return None # Note that we've caught the exception here, thus if python enters this block of code it will returns None. def get_int_number() -> int: while True: try: return int(input("Please enter a number: ")) except ValueError: print("Oops! That was no valid integer. Try again...") def calc() -> None: numerator = get_int_number() nominator = get_int_number() try: result = divide_numbers(numerator, nominator) print(result) except Exception as e: print("Calc function's except block:") print(e) calc()
Note that though it might be hard to see at first but what is really happening here is something like this:
try: try: # ... except ValueError: # ... except Exception as e: # ...
except
blocks cannot catch each other’s errors. if you wanna do that you need to do it manually:try: pass except NameError as name_error: try: pass except Exception as any_error: pass
- You can also handle multiple exception in one
except
block:except (RuntimeError, TypeError, NameError): pass
try
Clause.
- Inside the
try
block we write the code that might encounter some exceptions. - Python runs this block of code first, or let’s say tries to execute it.
- Possible scenarios:
- Nothing goes south Python runs all the codes in this block and jumps to
finally
block if present. - An error, oh, now Python checks if we have defined an
except
block that matches the name of current exception. If it finds one it executes its block and then continues with thefinally
block. - An error without a handler, thus Python tries to pass down the error to the outer
try ... except
block. If there is none, or we ain’t handling it there either then it’s an unhandled exception. This causes our Python program to crash and terminate with that error.
- Nothing goes south Python runs all the codes in this block and jumps to
except
Clause.
- Breaking out of the normal flow of control of a code block.
- Handle errors or other exceptional conditions.
- An exception is raised.
- Python interpreter raises an exception when it detects a run-time error.
- Explicitly raise an exception with the
raise
statement. - Cannot repair the cause of the error and retry the failing operation. But we can re-execute the function again if the exception was raised in a function for example.
- If the exception is not handled at all, the interpreter terminates execution of the program.
finally
Clause.
- Optional.
- Cleanup code which does not handle the exception.
- In Real world apps it is useful for releasing external resources (such as files or network connections).
- Executed whether:
- An exception occurred or not in the preceding code.
- You have used a
return
,continue
, orbreak
statement inside yourtry
clause. - You’ve handled the exception gracefully or not.
- You have a except clause for the raised exception or not. In this case it will executes the codes inside
finally
and then reraise the exception. - An exception was raised in your
except
/else
clause or not. In the case of exception it will be reraised after it finishes executing thefinally
block.
- Here we can skip reraising errors by using a
return
,continue
, orbreak
statement.
[!CAUTION]
The
return
statement inside thefinally
takes precedence over thereturn
statement inside thetry
clause!def func(): try: return 1 finally: return 2 print(func()) # 2
[!TIP]
There are some objects with a defined standard clean-up actions after object is no longer needed, regardless of whether or not the operation using the object succeeded or failed. E.g.
with
statement is one of them.
else
Clause
- Optional.
- Defined after all
except
clauses. - Code that must be executed if the
try
clause does notraise
an exception. -
try: print("some code...") except Exception: print("...") else: print("won't be executed if any exception was occurred inside the try clause")
raise
- Allows the programmer to force a specified exception to occur.
raise Exception("Bad user input")
- It takes an optional argument.
- This argument indicates the exception to be raised.
- Should be an exception instance or an exception class. E.g.
BaseException
, orException
, or classes who derive from them. - BTW we can delegate the task of instantiating an exception instance to the Python interpreter:
raise ValueError # Same as raise ValueError()
But here we cannot pass any value to its
constructor
. - You can pass a string to it as its argument:
raise "SomeError"
- If no expressions are present,
raise
re-raises the exception that is currently being handled (AKA active exception). This is useful when you just wanna log something somewhere that this exception occurred:try: result = 1 / 0 except ZeroDivisionError as e: raise
- Called on its own with not active exception or an exception in front of it? a
RuntimeError
exception will be raised.raise
Exception Chaining
try:
open("somefile.txt")
except OSError:
raise RuntimeError("unable to handle error")
- Here we will see the
Traceback
of both exceptions raised. One which isFileNotFoundError
, raised by OS. And theRuntimeError
which we’ve raised. - Though you can also specify that an exception was a direct result of another exception with
from
clause. This is useful when we are handling multiple exception and are transforming them:except (OSError, ConnectionError) as e: raise RuntimeError from e
- To disable this chaining behavior we can say:
except OSError: raise RuntimeError from None
Dealing with Multiple Exceptions – ExceptionGroup
- Instead of raising the first exception we can collect all of them.
-
Useful in concurrency and parallelism.
- Also good for where it is desirable to continue execution and collect multiple errors.
def f():
excs = [OSError('error 1'), SystemError('error 2')]
raise ExceptionGroup('there were problems', excs)
try:
f()
except* OSError as e:
print("There were OSErrors")
except* SystemError as e:
print("There were SystemErrors")
Note how we discerned the exception inside a group with except*
. Though this is a contrived example but I guess this can give you a glimpse of what it would look like in a more realistic code:
excs = []
for test in tests:
try:
test.run()
except Exception as e:
excs.append(e)
if excs:
raise ExceptionGroup("Test Failures", excs)
Here for example we are running some tests and wanted to catch all the exceptions after we’ve executed all the tests and raise them all at once.