AlgoMaster Logo

Exception Basics

High Priority20 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

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.

What an Exception Actually Is

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.

How Python Signals an Error: Stack Unwinding

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.

Reading a Traceback

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:

  1. The last line is the most important. It has the exception class (IndexError) and the message (list index out of range). This tells you what went wrong.
  2. The file and line number above the message tell you where it 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.
  3. The `in <module>` part says you were at the top level of a file (not inside a function or a method). If you were inside a function called place_order, it would say in place_order.
  4. The `Traceback (most recent call last)` header is just a label. It also tells you the order: the call that was made earliest is at the top of the list, and the call that actually broke is at the bottom. So the bottom line is always the closest to the failure.

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 tracebackWhat it tells you
Traceback (most recent call last)Label, plus a hint about the order of the frames
File "...", line N, in func_nameWhere in your code, and inside which function
The code snippet after the frameThe exact source line that was running
^^^^ or ~~~^~~ markers (3.11+)The exact expression inside the line that failed
The last lineThe 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.

Why Crash vs Why Handle

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:

  • Let it crash. A small script, a one-off computation, a script meant to run once and fail loud if anything is wrong. Crashing is the right behavior because it surfaces the bug immediately.
  • Catch and handle. A long-running server, a batch job processing many items, an interactive program. You catch known failure modes (a missing file, a bad input, a network blip), respond to them, and only crash on bugs you genuinely didn't expect.

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.

Common Built-in Exceptions

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.

ExceptionWhat triggers itQuick example
ZeroDivisionErrorDividing a number by zero.total / people when people == 0
TypeErrorAn operation got a value of the wrong type."29.99" * 2.0
ValueErrorThe type is right but the value is invalid.int("three")
IndexErrorA sequence index is out of range.cart[5] on a 2-item list
KeyErrorA dictionary key is missing.product["stock"] when there's no "stock"
AttributeErrorAn object doesn't have the attribute you asked for.mouse.price when mouse has no price
NameErrorThe name you used isn't defined in the current scope.print(total) before total is assigned
FileNotFoundErrorA file path you tried to open doesn't exist.open("orders.csv") and the file is gone
ImportErrorAn 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.

ZeroDivisionError

Any 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.

TypeError

The 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.

ValueError

The 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."

IndexError

The 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.

KeyError

The 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.

AttributeError

The 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.

NameError

total 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'?

FileNotFoundError

Opening 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.

ImportError

Python 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.

"Error" vs "Exception": A Quick Note on Terminology

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.

Syntax Errors vs Runtime Exceptions

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.

KindWhen detectedHas a traceback?Catchable with try/except?
Syntax errorParsing timeNo (one frame)No (in normal files)
Runtime exceptionExecution timeYesYes

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.

What the Exception Object Carries

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.

Quiz

Exception Basics Quiz

10 quizzes