When something goes wrong while a Python program is running — division by zero, a missing file, a number that didn’t parse — Python raises an exception. If the exception isn’t handled, the program stops and prints a traceback.

A first exception

result: float = 10 / 0
print(result)
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    result: float = 10 / 0
                    ~~~^~~
ZeroDivisionError: division by zero

A few things to notice:

  • The exception has a type (ZeroDivisionError).
  • It has a message (“division by zero”).
  • The program stops before reaching print(result).
  • The traceback shows the file and line where the error happened.

Common built-in exceptions

You’ll meet these often:

ExceptionWhen it happens
ZeroDivisionErrorDividing by zero
ValueErrorA function got the right type but a bad value (int("hello"))
TypeErrorAn operation on the wrong type ("hi" + 5)
KeyErrorAccessing a dict key that doesn’t exist
IndexErrorAccessing a list position that doesn’t exist
AttributeErrorAccessing a method or attribute that doesn’t exist
FileNotFoundErrorOpening a file that isn’t there
NameErrorUsing a variable that hasn’t been defined
ImportError / ModuleNotFoundErrorImporting something that doesn’t exist

You don’t have to memorise all of them — Python tells you which one when it happens.

Reading a traceback

The most important part of a traceback is usually at the bottom:

Traceback (most recent call last):
  File "main.py", line 4, in <module>
    process_data(data)
  File "main.py", line 2, in process_data
    return data["count"] / data["total"]
KeyError: 'total'

Read from the bottom up:

  1. What went wrong — KeyError: 'total' (the dict doesn’t have a key called 'total').
  2. Where — line 2 inside process_data.
  3. Who called that — line 4 in <module> (the top-level script).

The chain of File "...", line N entries is called the call stack — it shows how the program got to the failing line.

Exceptions are objects

An exception is a regular Python object — a class. ZeroDivisionError and friends are classes; when one is raised, an instance is created and Python “throws” it up the call stack until something catches it.

e = ValueError("bad value")
print(type(e))         # <class 'ValueError'>
print(e.args)          # ('bad value',)
print(str(e))          # bad value

This object-oriented design is why you can catch exceptions, inspect them, and even create your own (later lessons).

Errors at compile time vs runtime

We saw in Section 1 that Python parses your file before running it. Some errors are caught at parse time:

  • Syntax errors — invalid code, like missing colons or brackets.
  • Indentation errors — wrong indentation.

These are different. They aren’t exceptions — they’re caught before the program ever starts. You can’t “handle” them at runtime; you have to fix the code.

Exceptions are for things that go wrong during execution, when the code itself is valid but the world isn’t cooperating.

Why handle exceptions instead of letting them crash?

Sometimes letting the program stop is the right choice. A script that fails to load its config probably should exit.

But often you want to:

  • Recover — show the user a friendly message and continue
  • Retry — try again, perhaps with different input
  • Log — record what failed without losing the rest of the work
  • Clean up — close files, release resources, save partial results

That’s what try/except is for, and that’s the next lesson.

What’s next

You know what an exception is. Next — how to catch one with try/except.

Toggle theme (T)