Programs break. A customer's cart has a typo, an order file goes missing, a discount divides by zero. Python's response to these situations is to raise an exception, an event that stops the normal flow of execution and bubbles up looking for someone to handle it. This lesson explains what exceptions are, how Python signals them, how to read the traceback you get on screen, and which built-in exceptions you'll hit most often in everyday code.
An exception is an event Python raises when something goes wrong at runtime. The word "exception" is a bit grand for what's happening; the simpler way to think about it is that Python ran into a situation it can't continue past with the current instructions, so it stops, packages up what went wrong, and hands that package up to whoever is willing to catch it.
Here's a small program that triggers one:
The program didn't print anything. It stopped at line 3. Python couldn't compute 100.0 / 0, so it raised a ZeroDivisionError, and because nothing in the program was set up to catch it, the interpreter printed the traceback and quit.
Two things are worth noticing right away. First, the line print(share_per_person) never ran. Once an exception is raised, the rest of the program below the failing line is skipped unless something catches the exception. Second, the message Python printed has structure. There's a "Traceback" header, a File "<string>", line 3 pointer, and a ZeroDivisionError: division by zero summary. We'll come back to reading that structure in detail.
In Python, exceptions are normal objects. ZeroDivisionError is a class. The thing that gets raised is an instance of that class, carrying a message and (sometimes) extra attributes. You can catch them, inspect them, re-raise them, and even define your own. This lesson covers the basics.
When a function call goes wrong, Python doesn't just print the line that broke. It walks back up the chain of function calls that led to the failure, building a list of where it went and where the exception was raised. That walk is called unwinding the stack, and the list of frames it produces is what you see in the traceback.
Save this as a file and run it:
The error happens deep inside split_bill, on total / people. Python doesn't show only that line. It also shows the line that called split_bill (line 10), and it would keep walking back if more callers were involved. That's the unwinding: as the exception travels up, Python records each frame it passes through.
This matters because the real cause of a bug is often not at the bottom of the traceback. The arithmetic on total / people failed, but the actual mistake was passing 0 for people at the call site. Reading from the bottom up tells you what went wrong; reading from the top down (or middle out) tells you why.
The two carets-and-tildes lines (^^^^^^^^^^^^^^^^^^^^ and ~~~~~~^~~~~~~~) are called error location markers. They were added in Python 3.11 and they highlight the exact expression that raised the exception. In the first frame they point at the whole call. In the second frame they pinpoint the total / people operation. If you're on an older Python, you won't see those markers, just the lines themselves.
The diagram walks through what just happened. The program flowed normally until the division line, at which point Python raised the exception and started unwinding. Each frame it left got added to the traceback. With no handler anywhere in the chain, the interpreter printed the trace and ended the program.
Tracebacks look intimidating the first few times. The format is actually quite regular once you know the parts. Take this short example:
Read it in this order:
IndexError) and the message (list index out of range). This tells you what went wrong.File "<string>", line 2 says "the second line of whatever was passed in." A real script would say something like File "/Users/you/checkout.py", line 12.place_order, it would say in place_order.A longer traceback with multiple frames follows the same rules, just with more entries between the header and the exception line. Here's the structure summarized:
| Part of the traceback | What it tells you |
|---|---|
Traceback (most recent call last) | Label, plus a hint about the order of the frames |
File "...", line N, in func_name | Where in your code, and inside which function |
| The code snippet after the frame | The exact source line that was running |
^^^^ or ~~~^~~ markers (3.11+) | The exact expression inside the line that failed |
| The last line | The exception class and the error message |
When you're debugging, the first thing to do with a traceback is read the last line to learn what type of error happened, then read the frame just above it to learn where. The middle frames are how Python got there, useful when the error is buried inside library code or several function calls deep.
When a program hits an exception with no handler, Python prints the traceback and exits. That's a crash. For a small script you ran in a terminal, a crash is fine. You see the error, you fix it, you run again.
For a web app handling thousands of orders, a crash is not fine. If one customer's order has a malformed price and the server crashes, every other customer's request dies too. The whole point of exception handling is to catch the problem at the right level, do something sensible (skip the bad order, log it, return a helpful error message), and keep running.
So there are roughly two situations:
The decision isn't "catch everything." Catching too much hides bugs that should be visible. The art is in catching the specific exceptions you can recover from, while letting the unexpected ones surface as real errors.
A first taste of the syntax, just so you've seen it:
Same program as before, but now the IndexError doesn't crash. The try block runs, hits the bad index, and Python jumps to the matching except block. The program continues, prints the friendly message, and exits cleanly. The point right now is just to show that the crash isn't inevitable.
Python ships with a long list of built-in exceptions. For an e-commerce app there are roughly a dozen you'll meet in the first week of writing real code. Knowing them by name and by trigger saves a lot of head-scratching.
| Exception | What triggers it | Quick example |
|---|---|---|
ZeroDivisionError | Dividing a number by zero. | total / people when people == 0 |
TypeError | An operation got a value of the wrong type. | "29.99" * 2.0 |
ValueError | The type is right but the value is invalid. | int("three") |
IndexError | A sequence index is out of range. | cart[5] on a 2-item list |
KeyError | A dictionary key is missing. | product["stock"] when there's no "stock" |
AttributeError | An object doesn't have the attribute you asked for. | mouse.price when mouse has no price |
NameError | The name you used isn't defined in the current scope. | print(total) before total is assigned |
FileNotFoundError | A file path you tried to open doesn't exist. | open("orders.csv") and the file is gone |
ImportError | An import failed to find the module or a name inside it. | import not_a_real_package |
Each row is a class. Each class triggers when Python detects the specific condition described, and the error message it carries tells you which value caused the trouble. We'll walk through a real example for each, since seeing the actual traceback is more useful than reading a description.
ZeroDivisionErrorAny division (/, //, %) by zero raises this. Comes up most often when you compute averages or split totals without first checking that the denominator is non-zero.
TypeErrorThe price came in as a string (maybe read from a CSV file or a form), and the code assumed it was a number. Python can multiply a string by an integer ("ha" * 3 gives "hahaha"), but not by a float, so it raises TypeError. The fix is to convert price to a float before doing arithmetic on it.
ValueErrorThe type is right (a string is a perfectly valid thing to hand to int()), but the value doesn't make sense as an integer. The English word "three" is not a number Python can parse. int("3") would work. ValueError is the go-to exception for "I expected a value of this type, but this particular value doesn't fit."
IndexErrorThe cart has two items, indexed 0 and 1. Asking for cart[5] reaches past the end. Lists, tuples, and strings all raise IndexError when you index past their length.
KeyErrorThe dict has "name" and "price" keys, no "stock". Asking for a missing key raises KeyError, with the missing key as the message. This is the dictionary cousin of IndexError. Use product.get("stock") to get None instead of raising, or product.get("stock", 0) to supply a default.
AttributeErrorThe class never sets self.price, so asking for mouse.price raises AttributeError. Comes up most often when you misspell an attribute name or rename one and miss a caller.
NameErrortotal was never assigned. Python looked through the local and global namespaces, didn't find it, and raised NameError. Typos in variable names are the most common trigger. Recent Python versions even suggest a similar name when one is close: did you mean: 'total'?
FileNotFoundErrorOpening a file that doesn't exist (or is in a different directory than you assumed). FileNotFoundError is a more specific subclass of OSError. You'll meet other subclasses (PermissionError, IsADirectoryError) in the file handling section.
ImportErrorPython couldn't find a module by that name on the import path. ModuleNotFoundError is a subclass of ImportError introduced in Python 3.6. Catching ImportError will catch both, so most code uses the parent. Comes up when you forget to install a package, when the module name is misspelled, or when a package's internals change between versions.
You'll hear both words used, often as if they mean the same thing. In Python they almost do, but not quite.
Inside the standard library, the base class for everything you can catch is called Exception. The classes that represent specific failures (ValueError, TypeError, FileNotFoundError) inherit from Exception, even though their names end in "Error." So Python's own taxonomy treats "error" as a kind of exception, not as a separate thing.
The full hierarchy actually starts one level up at a class called BaseException, which is Exception's parent. A small number of system-level events (like the Ctrl+C interrupt) inherit from BaseException directly, not from Exception, specifically so that ordinary except Exception handlers don't accidentally swallow them.
In everyday speech, "error" and "exception" are usually interchangeable. When someone says "the program threw an error," they mean an exception was raised. The naming convention of ending built-in classes with "Error" is just a habit, not a category. The one place to be precise is in code: write except ValueError: (the class) and raise ValueError(...) (the class again). The word "exception" is for talking about the concept; the word "error" is part of the class names.
Not every problem Python reports is an exception. There's a separate category called syntax errors, and they happen earlier in the process, before your program runs at all.
Look at what's missing: the line doesn't say "Traceback (most recent call last)." Python never started running the program. The parser read the source code, saw that the if line was missing its colon, and refused to produce executable code. No code from this file ran.
Compare this to a runtime exception:
This one starts with the traceback header. Python parsed the code successfully (the syntax is fine), started executing it, and only then noticed there's no undefined_variable in scope. By that point the program is already running, so it raises NameError and unwinds the stack.
The distinction matters for two reasons. First, you can't catch a SyntaxError with try/except inside the same file, because the file never gets that far. (You can catch one when you're using exec or compile to evaluate code stored as a string at runtime, which is rare.) Second, fixing them is different work: a syntax error means edit the source until Python can parse it, while a runtime exception usually means handle the bad input or fix the bug that produced it.
| Kind | When detected | Has a traceback? | Catchable with try/except? |
|---|---|---|---|
| Syntax error | Parsing time | No (one frame) | No (in normal files) |
| Runtime exception | Execution time | Yes | Yes |
Technically, SyntaxError is itself a Python exception class (it inherits from Exception), which is why you'll see it written in CamelCase like all the others. The reason you can't catch it in the same file is mechanical, not philosophical: the file never compiles, so there's no try block in place to catch anything when the failure happens. A useful mental check: if you saved the file and didn't even run it, would you get an error? If yes, it's a syntax error. If the error only shows up after you run it (and especially if it depends on the input data), it's a runtime exception. Code that looks fine but has bad data flowing through it is the everyday case, and that's the case the rest of this section is about.
When an exception is raised, the thing Python is actually working with is an object. It has a class (which tells you the type of error) and at least one attribute called args, which holds the arguments passed when the exception was created. Most built-in exceptions also have a string message that summarizes what went wrong; that's what gets printed at the end of the traceback.
You won't usually inspect the exception object until you're inside a try/except block, but it's worth knowing the shape now.
The exception is a real Python object. type(caught).__name__ tells you the class name. caught.args is a tuple of the arguments the exception was constructed with, in this case a single string. Calling str(caught) gives you the message, which is the same string you'd see printed at the end of an uncaught traceback. Other built-in exceptions add their own attributes: a KeyError carries the missing key, a FileNotFoundError carries the path that wasn't found, and so on. These are the details that handlers use to decide what to do.
The takeaway: an exception isn't just a printed string, it's a structured object you can examine and react to once you start writing handlers.
A related point: every built-in exception class inherits from Exception (and Exception itself inherits from BaseException). That's why you can write except Exception to catch any normal error in one block. Catching the most general class is rarely what you want, because it hides bugs, but it's a useful guardrail in places like a top-level request handler that should log and move on rather than crash.
10 quizzes