Programming and IT

Python: exceptions

An exception is a signal that execution went differently from what the code expected. By default it stops the program and prints a traceback. A `try/except` block lets you catch only the error you anticipated and carry on, without hiding real breakages.

Updated
In this article

What an error looks like and what to read in it#

print(int("hundred"))
  # Traceback (most recent call last):
  #   File "<stdin>", line 1, in <module>
  # ValueError: invalid literal for int() with base 10: 'hundred'

Two things matter in a traceback: the last line — the error type and message — and the lines above it, which show the path that led there. Read it from the bottom up: first what happened, then where.

Exception types form a tree. Almost everything you catch in application code inherits from Exception:

  • ValueError — the type is right, the value is not (int("hundred"));
  • TypeError — the type is wrong ("a" + 1);
  • KeyError and IndexError — no such key or index;
  • ZeroDivisionError — division by zero;
  • FileNotFoundError — no such file;
  • AttributeError — the object has no such attribute.

try/except: the minimum#

try:
    age = int(input("age: "))
except ValueError:
    print("a whole number is required")
    age = 0
print(age)

The try block holds only the risky line — the shorter it is, the clearer it is what exactly is protected. You can list several types, and the error object itself is available through as:

data = {"a": 1}
try:
    print(data["b"] / 0)
except (KeyError, ZeroDivisionError) as error:
    print(type(error).__name__, "—", error)
  # KeyError — 'b'

Several except blocks are checked from top to bottom, and the first matching one runs. That is why specific types go above general ones: an except Exception placed first catches everything, and the rest never get a chance.

else and finally#

try:
    value = int("42")
except ValueError:
    print("not a number")
else:
    print("parsed:", value)     # only if there was no exception
finally:
    print("this block always runs")
  # parsed: 42
  # this block always runs

else runs when try finished without errors — put code there that should not be covered by this except. finally runs no matter what: on an error, on a return, and when an exception propagates further. It is where you release resources — although for files and connections the with statement is handier, as described in the article on reading files in Python.

raise and your own error classes#

Raising an exception yourself takes one line. It is the right way to report invalid arguments: a function should not silently return None.

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError(f"amount must be positive, got {amount}")
    if amount > balance:
        raise ValueError("insufficient funds")
    return balance - amount

try:
    withdraw(100, -5)
except ValueError as error:
    print("refused:", error)
  # refused: amount must be positive, got -5

When the built-in types are not enough, you define your own — usually as an empty class:

class ConfigError(Exception):
    """The settings are invalid."""

def load(cfg):
    if "host" not in cfg:
        raise ConfigError("the settings have no host key")

try:
    load({})
except ConfigError as error:
    print(error)      # the settings have no host key

To catch an error and re-raise it while keeping the cause, use raise ... from: raise ConfigError("could not read") from error — both exceptions stay in the traceback, and you can see which one was the root cause.

What not to do#

A bare except:. Such a block catches absolutely everything, including KeyboardInterrupt and SystemExit — the program can no longer be stopped from the keyboard. If you need a wide net, write except Exception, and preferably report what happened.

except ...: pass. A silently swallowed error comes back an hour later as an empty report, and there is nothing left to trace it with. At the very least, print it or write it to a log.

A huge try. When there are twenty lines under try, except ValueError applies to all twenty, and you will catch an error somewhere you did not expect.

Exceptions instead of conditions. A check like if x > 0 does not need try. But the opposite extreme is harmful too: Python favours trying and catching over asking first — d.get(key) or try/except KeyError beats if key in d before every access when there are many accesses.

Practice: write a function safe_div(a, b) that returns the quotient, or None with a message on division by zero; then a function parse_ages(strings) that turns a list of strings into numbers, skipping non-numeric ones and reporting each skip. You will need Python lists and Python functions.

Step-by-step plan

  1. Read a tracebackRun int("hundred") and take the output apart: type, message, location.
  2. Your first try/exceptProtect a string-to-number conversion and print a clear message instead of crashing.
  3. Several typesCatch KeyError and ZeroDivisionError in one block and print the error type with type(error).__name__.
  4. Add finallyMake sure the finally block runs both with and without an error.
  5. Your own error classDefine a subclass of Exception, raise it from a function and catch it in the calling code.

Start learning this in your own space

The plan goes into your repository: tick off stages, keep notes — the change history shows how far you have come.

Start the plan

Check yourself

1.Which exception type does int("hundred") raise?

2.try: x = 1 / 0 / except ZeroDivisionError: x = 7 / finally: x += 1 — what is x?

3.Which block runs only when there was no exception in try?

4.What is wrong with a bare except: without a type?

Sources

Was this helpful?

More articles

Programming and IT How to learn Python from scratch Python is a good first programming language: code reads almost like text, and the standard library covers most everyday tasks. This plan takes you from installing the interpreter to your own scripts covered by tests in about four months, at roughly an hour a day. Programming and IT Python: slicing A slice cuts a piece out of a sequence by the rule "from which index, up to which, with what step". The notation is compact, but it has two places where almost everyone slips: the right bound is not included, and with a negative step the bounds swap places. Let us go through both. Programming and IT Python: generators A generator hands out values one at a time, and only when someone asks for them. It does not keep the whole sequence in memory, so it suits large files and endless streams. The price is that a generator is single-use: you cannot loop over it twice. Programming and IT Python: regular expressions A regular expression is a pattern that describes a set of strings. In Python you work with them through the `re` module from the standard library. You write the pattern as a string with an `r` prefix, search with `re.search` and pull the result out of a match object. Everything else is details of the pattern language. Programming and IT Python: lists A list is a mutable, ordered collection of values of any type. It is also the main source of surprises for beginners: assignment does not copy a list, sort returns nothing, and removing items inside a loop skips half of them. Let us go through it in order. Programming and IT Python: classes A class describes what data an object is made of and what it can do. The `__init__` constructor fills in a new object, and `self` is a reference to the object itself. Below: a minimal working class, the difference between class and instance attributes, and ways to make the code shorter.

More solutions