Is it a good practice to use try-except-else in Python? Is it a good practice to use try-except-else in Python? python python

Is it a good practice to use try-except-else in Python?


"I do not know if it is out of ignorance, but I do not like that kind of programming, as it is using exceptions to perform flow control."

In the Python world, using exceptions for flow control is common and normal.

Even the Python core developers use exceptions for flow-control and that style is heavily baked into the language (i.e. the iterator protocol uses StopIteration to signal loop termination).

In addition, the try-except-style is used to prevent the race-conditions inherent in some of the "look-before-you-leap" constructs. For example, testing os.path.exists results in information that may be out-of-date by the time you use it. Likewise, Queue.full returns information that may be stale. The try-except-else style will produce more reliable code in these cases.

"It my understanding that exceptions are not errors, they should only be used for exceptional conditions"

In some other languages, that rule reflects their cultural norms as reflected in their libraries. The "rule" is also based in-part on performance considerations for those languages.

The Python cultural norm is somewhat different. In many cases, you must use exceptions for control-flow. Also, the use of exceptions in Python does not slow the surrounding code and calling code as it does in some compiled languages (i.e. CPython already implements code for exception checking at every step, regardless of whether you actually use exceptions or not).

In other words, your understanding that "exceptions are for the exceptional" is a rule that makes sense in some other languages, but not for Python.

"However, if it is included in the language itself, there must be a good reason for it, isn't it?"

Besides helping to avoid race-conditions, exceptions are also very useful for pulling error-handling outside loops. This is a necessary optimization in interpreted languages which do not tend to have automatic loop invariant code motion.

Also, exceptions can simplify code quite a bit in common situations where the ability to handle an issue is far removed from where the issue arose. For example, it is common to have top level user-interface code calling code for business logic which in turn calls low-level routines. Situations arising in the low-level routines (such as duplicate records for unique keys in database accesses) can only be handled in top-level code (such as asking the user for a new key that doesn't conflict with existing keys). The use of exceptions for this kind of control-flow allows the mid-level routines to completely ignore the issue and be nicely decoupled from that aspect of flow-control.

There is a nice blog post on the indispensibility of exceptions here.

Also, see this Stack Overflow answer: Are exceptions really for exceptional errors?

"What is the reason for the try-except-else to exist?"

The else-clause itself is interesting. It runs when there is no exception but before the finally-clause. That is its primary purpose.

Without the else-clause, the only option to run additional code before finalization would be the clumsy practice of adding the code to the try-clause. That is clumsy because it risksraising exceptions in code that wasn't intended to be protected by the try-block.

The use-case of running additional unprotected code prior to finalization doesn't arise very often. So, don't expect to see many examples in published code. It is somewhat rare.

Another use-case for the else-clause is to perform actions that must occur when no exception occurs and that do not occur when exceptions are handled. For example:

recip = float('Inf')try:    recip = 1 / f(x)except ZeroDivisionError:    logging.info('Infinite result')else:    logging.info('Finite result')

Another example occurs in unittest runners:

try:    tests_run += 1    run_testcase(case)except Exception:    tests_failed += 1    logging.exception('Failing test case: %r', case)    print('F', end='')else:    logging.info('Successful test case: %r', case)    print('.', end='')

Lastly, the most common use of an else-clause in a try-block is for a bit of beautification (aligning the exceptional outcomes and non-exceptional outcomes at the same level of indentation). This use is always optional and isn't strictly necessary.


What is the reason for the try-except-else to exist?

A try block allows you to handle an expected error. The except block should only catch exceptions you are prepared to handle. If you handle an unexpected error, your code may do the wrong thing and hide bugs.

An else clause will execute if there were no errors, and by not executing that code in the try block, you avoid catching an unexpected error. Again, catching an unexpected error can hide bugs.

Example

For example:

try:    try_this(whatever)except SomeException as the_exception:    handle(the_exception)else:    return something

The "try, except" suite has two optional clauses, else and finally. So it's actually try-except-else-finally.

else will evaluate only if there is no exception from the try block. It allows us to simplify the more complicated code below:

no_error = Nonetry:    try_this(whatever)    no_error = Trueexcept SomeException as the_exception:    handle(the_exception)if no_error:    return something

so if we compare an else to the alternative (which might create bugs) we see that it reduces the lines of code and we can have a more readable, maintainable, and less buggy code-base.

finally

finally will execute no matter what, even if another line is being evaluated with a return statement.

Broken down with pseudo-code

It might help to break this down, in the smallest possible form that demonstrates all features, with comments. Assume this syntactically correct (but not runnable unless the names are defined) pseudo-code is in a function.

For example:

try:    try_this(whatever)except SomeException as the_exception:    handle_SomeException(the_exception)    # Handle a instance of SomeException or a subclass of it.except Exception as the_exception:    generic_handle(the_exception)    # Handle any other exception that inherits from Exception    # - doesn't include GeneratorExit, KeyboardInterrupt, SystemExit    # Avoid bare `except:`else: # there was no exception whatsoever    return something()    # if no exception, the "something()" gets evaluated,    # but the return will not be executed due to the return in the    # finally block below.finally:    # this block will execute no matter what, even if no exception,    # after "something" is eval'd but before that value is returned    # but even if there is an exception.    # a return here will hijack the return functionality. e.g.:    return True # hijacks the return in the else clause above

It is true that we could include the code in the else block in the try block instead, where it would run if there were no exceptions, but what if that code itself raises an exception of the kind we're catching? Leaving it in the try block would hide that bug.

We want to minimize lines of code in the try block to avoid catching exceptions we did not expect, under the principle that if our code fails, we want it to fail loudly. This is a best practice.

It is my understanding that exceptions are not errors

In Python, most exceptions are errors.

We can view the exception hierarchy by using pydoc. For example, in Python 2:

$ python -m pydoc exceptions

or Python 3:

$ python -m pydoc builtins

Will give us the hierarchy. We can see that most kinds of Exception are errors, although Python uses some of them for things like ending for loops (StopIteration). This is Python 3's hierarchy:

BaseException    Exception        ArithmeticError            FloatingPointError            OverflowError            ZeroDivisionError        AssertionError        AttributeError        BufferError        EOFError        ImportError            ModuleNotFoundError        LookupError            IndexError            KeyError        MemoryError        NameError            UnboundLocalError        OSError            BlockingIOError            ChildProcessError            ConnectionError                BrokenPipeError                ConnectionAbortedError                ConnectionRefusedError                ConnectionResetError            FileExistsError            FileNotFoundError            InterruptedError            IsADirectoryError            NotADirectoryError            PermissionError            ProcessLookupError            TimeoutError        ReferenceError        RuntimeError            NotImplementedError            RecursionError        StopAsyncIteration        StopIteration        SyntaxError            IndentationError                TabError        SystemError        TypeError        ValueError            UnicodeError                UnicodeDecodeError                UnicodeEncodeError                UnicodeTranslateError        Warning            BytesWarning            DeprecationWarning            FutureWarning            ImportWarning            PendingDeprecationWarning            ResourceWarning            RuntimeWarning            SyntaxWarning            UnicodeWarning            UserWarning    GeneratorExit    KeyboardInterrupt    SystemExit

A commenter asked:

Say you have a method which pings an external API and you want to handle the exception at a class outside the API wrapper, do you simply return e from the method under the except clause where e is the exception object?

No, you don't return the exception, just reraise it with a bare raise to preserve the stacktrace.

try:    try_this(whatever)except SomeException as the_exception:    handle(the_exception)    raise

Or, in Python 3, you can raise a new exception and preserve the backtrace with exception chaining:

try:    try_this(whatever)except SomeException as the_exception:    handle(the_exception)    raise DifferentException from the_exception

I elaborate in my answer here.


Python doesn't subscribe to the idea that exceptions should only be used for exceptional cases, in fact the idiom is 'ask for forgiveness, not permission'. This means that using exceptions as a routine part of your flow control is perfectly acceptable, and in fact, encouraged.

This is generally a good thing, as working this way helps avoid some issues (as an obvious example, race conditions are often avoided), and it tends to make code a little more readable.

Imagine you have a situation where you take some user input which needs to be processed, but have a default which is already processed. The try: ... except: ... else: ... structure makes for very readable code:

try:   raw_value = int(input())except ValueError:   value = some_processed_valueelse: # no error occured   value = process_value(raw_value)

Compare to how it might work in other languages:

raw_value = input()if valid_number(raw_value):    value = process_value(int(raw_value))else:    value = some_processed_value

Note the advantages. There is no need to check the value is valid and parse it separately, they are done once. The code also follows a more logical progression, the main code path is first, followed by 'if it doesn't work, do this'.

The example is naturally a little contrived, but it shows there are cases for this structure.