Basics

Exceptions in Python: try, except, else, finally, raise and Custom Exceptions

Updated: · Skillaria.Top

An exception in Python is an error object that interrupts the normal flow of code until a try/except block catches it; else runs only if no error occurred, and finally always runs. You raise an exception yourself with the raise statement, for example raise ValueError("amount is negative"), and create your own with a class that inherits from Exception.

An exception in Python is an object that signals an error and interrupts the normal flow of a program. If nobody catches it, Python prints a traceback and stops. Exceptions are caught with the try/except construct and raised with the raise statement. Let us look at how this works, which errors occur most often and how to avoid turning error handling into hiding errors.

What an exception is in Python

When an operation cannot complete — division by zero, access to a missing key, turning a word into a number — Python creates an exception object and "throws" it. The current function stops, and the exception travels up the chain of calls until it meets a suitable handler. If none is found, the program crashes.

>>> 10 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

By the way, the answer to a common question: dividing by zero in Python raises a ZeroDivisionError exception. This applies to /, // and % alike.

How to read a traceback

A traceback is a report of how the program arrived at the error. It is easier to read from the bottom up. The last line is the main thing: the exception type and the message. Above it is the line of code where it happened, and higher still is who called it, all the way to the entry point.

Traceback (most recent call last):
  File "tracker.py", line 9, in <module>
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: 'hundred'

Since Python 3.11, markers such as ~~~^^^ are drawn under the line of code (PEP 657): they show exactly which expression in the line failed. This helps especially when several calls follow one another on the same line.

"Did you mean" hints

Since Python 3.10, a typo in a name produces an error that suggests a close alternative. In Python 3.15 the hints have become smarter: the interpreter knows the habits from other languages. Calling list.push will suggest append, and "coffee".toUpperCase() will suggest the upper method. The hint also looks into nested attributes: if a dictionary lives in the totals attribute of an object and you call items() on the object itself, the error will suggest .totals.items. delattr() now also suggests close names.

Exception types in Python

There are dozens of built-in exceptions, but at the start you will most often see these:

ExceptionWhen it occursExample
NameErrorA name is not defined, most often a typoprint(totl)
TypeErrorThe operation does not suit this type"5" + 3
ValueErrorThe type is right, the value is notint("hundred")
KeyErrorThere is no such key in the dictionaryd["missing"]
IndexErrorThe index is outside the list[1, 2][5]
AttributeErrorThe object has no such method or attribute[].push(1)
ZeroDivisionErrorDivision by zero1 / 0
FileNotFoundErrorThe file was not foundopen("nope.txt")

SyntaxError deserves a separate mention (and its special case IndentationError): it occurs while the file is being parsed, before the code runs, so you cannot catch it with try in the same file.

The exception hierarchy

Exceptions are classes, and they are arranged in a hierarchy. At the top is BaseException. It is inherited by Exception (almost all "ordinary" errors) and several system ones: KeyboardInterrupt, SystemExit, GeneratorExit.

BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError

The hierarchy matters when catching: except LookupError will catch both KeyError and IndexError. And KeyboardInterrupt is not a bug in the code: it is how Python reports that the user pressed Ctrl+C. It deliberately does not inherit from Exception, so that except Exception does not prevent you from stopping the program.

Handling exceptions: try/except/else/finally

Code that may fail goes into a try block. The except block names the error type and what to do with it. The except block is exactly what Python uses for handling exceptions.

def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"Skipping the line: {e}")
        return None
    else:
        return value          # runs if there was no error
    finally:
        print("parsing finished")   # always runs

print(parse_amount("250"))

The output order here is: first "parsing finished", then 250.0. The return in the else block remembers the value, then Python necessarily runs finally and only after that returns the result. That is why finally is reliable for cleanup: closing a file, releasing a connection.

If you write your own return in finally, it overrides the previous one and can even "swallow" the exception. Since Python 3.14 such code triggers a SyntaxWarning (PEP 765) — do not write return in finally.

Several exceptions at once

You can write several except blocks for different types, or list the types in a tuple:

try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("Invalid amount:", e)
    amount = 0

The blocks are checked from top to bottom, and the first matching one runs, so more specific types go above general ones. Since Python 3.14 (PEP 758) several types can be listed without brackets — except ValueError, TypeError: — but if there is an as clause, the brackets are still required.

How to print the error message and find its type

The exception object is available through as e. str(e) gives the message, type(e).__name__ gives the name of the type:

try:
    int("hundred")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: 'hundred'

The traceback module: the full error text

Sometimes you need to catch an error but keep the full traceback — for a log or a report. The traceback module is made for this:

import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # prints the traceback to stderr
    text = traceback.format_exc()     # the same text as a string

If you write logs with the logging module, it is simpler to call logging.exception("could not calculate the total") inside except — the traceback is added to the message automatically.

Python raise: how to raise an exception

The raise statement throws an exception manually. This is how a function reports that it cannot work with such data:

def add_expense(amount):
    if amount < 0:
        raise ValueError(f"The amount cannot be negative: {amount}")
    ...

add_expense(-5)
# ValueError: The amount cannot be negative: -5

Python raise is a statement, not a function, so no brackets are needed after the word raise: the brackets contain the arguments passed to the exception constructor. Choose the type by meaning: a wrong value is ValueError, a wrong type is TypeError.

raise without arguments and raise from

Inside except, a bare raise re-throws the current exception unchanged. You need this when you want to log something but leave the handling to the calling code.

If you replace one exception with another, use raise ... from e — the original error is kept in the chain, and the traceback shows both:

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"Config file not found: {path}") from e

add_note: adding context to an error

Since Python 3.11 (PEP 678) you can attach a note to an exception with the add_note() method and re-raise it. The error type does not change, and the note is printed below the message:

def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"file expenses.csv, line {line_no}")
        raise

parse("hundred", 17)
# ValueError: could not convert string to float: 'hundred'
# file expenses.csv, line 17

How to create your own exception in Python

Custom exceptions are needed for errors of your problem domain: the budget is exceeded, the order is already paid, the user is blocked. A subclass of Exception is enough:

class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: limit {limit}, spent {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("Food", 10000, 12500)
except BudgetExceeded as e:
    print("Warning:", e)   # Warning: Food: limit 10000, spent 12500

Now the calling code catches exactly the budget overrun and does not confuse it with typos. In a large project it is handy to create a common base class (class TrackerError(Exception)) and inherit all your exceptions from it — then they can all be caught with a single except TrackerError. Inherit from Exception, not from BaseException.

Exception groups and except*

Sometimes several errors happen at once — for example when checking a whole file, or in parallel tasks of asyncio.TaskGroup. Since Python 3.11 they can be collected into an ExceptionGroup and handled by type with except*:

def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("errors in the file", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"Invalid rows: {len(eg.exceptions)}")   # Invalid rows: 2

Python try except all errors: why you should not do this

The most harmful habit is catching everything: except: pass. The program "stops crashing", but in fact it stops reporting problems. A bare except even catches KeyboardInterrupt and SystemExit, so the program cannot be stopped with Ctrl+C. except Exception: pass is slightly better, but it still silently swallows typos in names and logic errors.

How to handle exceptions correctly in Python:

  • catch only the type you know how to handle: except ValueError, not everything;
  • keep the try block short — only the lines that can fail in that particular way;
  • if you catch broadly (except Exception) at the top level of a program, always log the traceback;
  • do not use exceptions for ordinary branching where a check is enough: dict.get() instead of try/except KeyError in simple cases;
  • for resources prefer with — it closes the file itself even when an error occurs.

Summary

Exceptions in Python are a normal mechanism for reporting errors, not a reason to hide them. Read the traceback from the bottom up, catch specific types in try/except, use else for code that runs "if everything went well" and finally for cleanup. Python raise lets you report an error yourself, while raise from and add_note preserve context, and your own exceptions inherit from Exception. In our Python course this topic is practised hands-on: the expense tracker stops crashing because of one broken CSV line and expresses a budget overrun with its own exception.

FAQ

How is an error different from an exception in Python?

In everyday speech the two are synonyms. Technically, a SyntaxError is detected while the code is parsed, while other errors occur during execution as exception objects that can be caught.

Can execution continue from the same place after an exception?

No. After it is caught, execution continues after the try/except block, not from the line where the error occurred. If you need to process each line of a file, put the try inside the loop.

What happens if an exception occurs inside except?

It propagates further, and the original exception is kept in its __context__ attribute. The traceback will show the phrase "During handling of the above exception, another exception occurred".

How do I check in tests that a function raises an exception?

In pytest use the construct with pytest.raises(ValueError):, in unittest use self.assertRaises(ValueError). The test passes only if an exception of the right type occurred inside the block.

What is assert and how does it differ from raise?

assert condition, "message" raises AssertionError if the condition is false. It is a development check tool: when run with the -O flag, asserts are disabled, so user input must be validated with if and raise.

Learn this topic on video

This topic is covered in depth with practice in the course “Python 3.15: от новичка до профи”.

Go to course