Every real program meets bad input, missing files and failing networks. Python reports these problems by raising an exception: an object that describes what went wrong and travels up the chain of function calls until some code handles it. If nothing handles it, the program stops and prints a traceback.
This unit shows you how to read those tracebacks, how to handle the errors you expect, how to signal your own errors, and how to make sure cleanup code always runs.
You will be able to:
- Read a traceback from the bottom up and find the line that failed
- Handle specific exceptions with
try,except,elseandfinally, and say when each block runs - Raise, reraise and chain exceptions, and define your own exception classes
- Choose between the EAFP and LBYL styles, and use
withto release resources - Report several errors at once with
ExceptionGroupandexcept* - Use
assert,warningsandlogging.exceptionfor the jobs they are meant for
Syntax errors and exceptions
There are two kinds of error. A syntax error means the code is not valid Python. The parser finds it before a single line runs, so nothing in the file executes:
Python points at the problem and stops with SyntaxError: '(' was never closed. Fix the code and run it again; you cannot catch a syntax error in the file that contains it.
An exception happens while valid code runs: dividing by zero, converting "abc" to int, opening a file that is not there. Exceptions are objects, and each one has a type such as ValueError or FileNotFoundError.
Reading a traceback
Saved as report.py and run, it prints this (the file path is shortened):
Read it from the bottom up:
- The last line gives the exception type and message:
ZeroDivisionError: division by zero. - The frame above it is where the error happened: line 3, inside
average. The~and^marks underline the failing expression, and the^characters point at the operation that raised (here the/). - Each earlier frame is a caller. "Most recent call last" means the outermost call is at the top.
The bug is usually in your own code nearest the bottom. Here average received an empty list, and the real question is whether report should ever pass one.
Handling exceptions with try and except
Put the code that might fail in a try block and the recovery code in an except block that names the exception type:
as err binds the exception object, so you can read its message. Python deletes the name err when the except block ends.
A try can have several except clauses. Python checks them top to bottom and runs only the first one that matches. A clause matches its own type and every subclass of it, so list specific types (KeyError) before their parents (LookupError).
To handle several types the same way, list them together. The parenthesised tuple works on every Python 3 version. Since Python 3.14 (PEP 758) you may leave the parentheses out, but only when there is no as. The example uses the newer form, so it needs Python 3.14 or later:
Note. Use the parenthesised form if your code must also run on Python 3.13 or earlier.
else and finally
A try statement has two optional extra blocks:
elseruns only if thetryblock raised nothing.finallyruns always: after success, after a handled exception, when a handler or theelseblock raises, when an exception keeps propagating, and when the function returns early.
Why else instead of putting more code inside try? Keep the try block as small as possible, so you only catch errors from the one operation you expect to fail. Code in else runs only on success, and its own errors are not swallowed by the handler above it.
finally even runs when the try block returns:
Pitfall. Never leave a
finallyblock withreturn,breakorcontinue. It silently throws away any exception in flight and overrides thetryblock's return value. Since Python 3.14 (PEP 765) the compiler warns about it.
When Python 3.14+ compiles this file, it prints a warning to stderr ending in SyntaxWarning: 'return' in a 'finally' block. The program still runs: the caller gets an empty dict and never learns the file was corrupt.
Catch what you can handle
All built in exceptions form a class hierarchy. An except clause for a class also catches every subclass.
except Exception: catches everything under Exception. It does not catch Ctrl+C (KeyboardInterrupt) or sys.exit() (SystemExit). A bare except: catches those too, which is why PEP 8 warns against it.
A bare except: would have caught the SystemExit and kept a program running that was asked to stop. Even except Exception is too wide most of the time, because it also hides your own bugs:
The NameError from the typo is swallowed, and the function returns a wrong answer without complaint. Catch the narrowest type you can actually handle (ZeroDivisionError here). Reserve except Exception for the outermost layer of a program, where you log the error and carry on with the next request or job.
Raising exceptions
Use raise to signal that your function cannot do its job. Pick the built in type that fits (ValueError for a bad value, TypeError for a wrong type) and write a message that says what was expected and what arrived:
Notice the split of jobs: the function raises, and its caller decides what to do. A function that raises and catches its own exception gains nothing over an if.
Reraising and chaining
Inside an except block you have four ways to raise:
| You write | Effect | The traceback says |
|---|---|---|
raise | reraises the current exception unchanged | the original traceback |
raise NewError(...) from err | explicit chaining; err is stored in __cause__ | "The above exception was the direct cause of the following exception" |
raise NewError(...) | implicit chaining; stored in __context__ | "During handling of the above exception, another exception occurred" |
raise NewError(...) from None | hides the original | only the new exception |
Use a bare raise when you want to log or clean up and then let the error continue. Use from err when you translate a low level error into one that fits your function:
Adding a note
Since Python 3.11, add_note() attaches extra context to an exception without changing its type. Tracebacks print the notes under the message:
Custom exceptions
Your own exception type is a class that inherits from Exception. It can be a single statement; the docstring is its body. Name it with an Error suffix (PEP 8). Classes and objects explains classes in full, but you do not need that unit to write these:
A small hierarchy like this lets callers choose: catch MissingSettingError alone, or every ConfigError at once.
An exception can also carry data. Give it an initialiser that stores the extra values, and pass the message on to the parent class with super().__init__(...). Classes and objects explains both ideas; for now you can copy the shape:
The text passed to super().__init__() is what str(err) and the traceback show. The extra attribute lets the code that catches the error read the value without picking the message apart.
Several errors at once: ExceptionGroup and except*
Sometimes several independent things fail together, for example when you validate every field of a form or run tasks concurrently. Since Python 3.11, ExceptionGroup bundles them, and except* handles each type inside the group:
Unlike except, several except* clauses can run for one group, and each receives a smaller group holding only its matching exceptions. You cannot mix except and except* in one try. You will meet exception groups again with asyncio.TaskGroup in Concurrency.
EAFP and LBYL
There are two styles for code that might fail:
- LBYL, "look before you leap": check first, then act.
- EAFP, "easier to ask forgiveness than permission": act, and handle the exception if it comes.
Python code leans towards EAFP. For files it is also more correct: between an if path.exists() check and the open() call, another program can delete the file, so the check proves nothing. LBYL is fine when the check is cheap and failure is common, and often a method such as dict.get removes the need for either.
Context managers and with
A context manager is an object that sets something up when a with block starts and tears it down when the block ends, even if the block raises. You have already met one: with open(...) closes the file for you. Behind the scenes, with calls the object's __enter__ and __exit__ methods.
The file is closed as soon as the block ends, whether it ends normally or with an exception. Files and data formats covers open() in full.
The easiest way to write your own is contextlib.contextmanager. Everything before yield is setup, the yielded value is bound by as, and the finally part is the teardown:
The teardown ran in both cases, and the RuntimeError still reached the outer try. A with block cleans up; it does not hide errors.
contextlib.suppress ignores the exception types you name, for the cases where "it was already gone" is fine:
Tip. For this exact case
cache.unlink(missing_ok=True)is shorter. Reach forsuppresswhen no such option exists.
assert is for bugs, not for input
assert condition, message raises AssertionError when the condition is false. It documents an assumption about your own code. Python removes every assert when it runs with the -O option, so never use it to validate data from users, files or networks:
Validate input with an if and a raise ValueError(...), as set_volume does above. Keep assert for invariants and for tests (Testing with pytest).
Warnings and logging
A warning tells the caller about something that is not yet an error, such as a deprecated function. The program keeps running:
Normally a warning goes to stderr, once per code location. stacklevel=2 makes it point at the caller's line, which is the line that needs changing. catch_warnings is used here only to print it on stdout. By default Python shows a DeprecationWarning only when the code that triggers it is in the main script; run python -W default script.py to see all of them, or python -W error script.py to turn warnings into exceptions, which is useful in tests.
When you catch an exception you cannot fix, log it with logging.exception inside the except block. It logs at ERROR level and appends the full traceback:
Logging writes to stderr, so the log record does not appear in the output above. Your terminal also shows (path shortened):
A tour of the standard library covers logging configuration.
Check yourself
- When does Python report a
SyntaxError, and why can't atryin the same file catch it? - You have
except LookupError:aboveexcept KeyError:. What goes wrong? - What is the difference between code in the
elseblock and code at the end of thetryblock? - Why does PEP 8 discourage a bare
except:? Why isexcept Exception:still risky? - What do
raise,raise X from errandraise X from Noneeach do inside anexceptblock? - Why should you not validate user input with
assert? - What happens to an exception in flight if
finallyexecutesreturn, and what does Python 3.14+ do about it? - Two
except*clauses both match parts of anExceptionGroup. How many of them run?
Suggested answers
- While parsing, before any code in the file runs. The
trystatement itself never starts, so it cannot handle anything. KeyErroris a subclass ofLookupError, so the first clause catches everyKeyErrorand the second is never reached. Put specific types first.- Exceptions raised in
elseare not caught by that statement'sexceptclauses, andelseruns only after thetryblock succeeded. It keeps the guarded region small. - A bare
except:also catchesKeyboardInterruptandSystemExit, so the user cannot stop the program.except Exception:still hides programming mistakes such as aNameErrorfrom a typo. raisereraises the current exception unchanged.raise X from errraisesXwitherrstored as its direct cause.raise X from NoneraisesXand hides the original exception from the traceback.python -Oremoves everyassert, so the check would silently disappear in some deployments.- The exception is discarded and the function returns normally. Since 3.14 the compiler emits a
SyntaxWarningforreturn,breakorcontinuethat leaves afinallyblock. - Both. Each clause receives a group containing only the exceptions it matched.
Exercises
These exercises are optional practice. Pick the ones that help you.
- Ask for a number. Write
ask_int(prompt, low, high)that callsinput(prompt)until the user enters a whole number betweenlowandhighinclusive, then returns it. For text that is not a number print'<text>' is not a whole number; for a number out of range printplease enter a number from <low> to <high>. With inputsten,150,42and the range 1 to 100, the function returns 42. - Validation error. Define
ValidationError(ValueError)whose initialiser takesfieldandmessage, storesfieldas an attribute, and has the string form"<field>: <message>". Writevalidate_order(order: dict) -> dictthat raises it whenquantityis not anintof at least 1 (abooldoes not count), or whenemaillacks an@. Valid orders are returned unchanged. - Retry with backoff. Write
retry(func, attempts=4, base_delay=0.1, retry_on=(ConnectionError,), sleep=time.sleep). It callsfunc()and returns its result. Iffuncraises one ofretry_on, it sleepsbase_delay * 2 ** (n - 1)seconds after the nth failure and tries again. After the last attempt it reraises the last exception. Rejectattempts < 1withValueError. Test it with a fakesleepthat records the delays, so the tests run instantly. For a function that fails twice and then succeeds, the recorded delays are[0.1, 0.2]. - Safe file read. Write
read_text_safely(path, default=""). It returns the file's text decoded as UTF-8; returnsdefaultif the file does not exist; and raisesValueError("<name> is not valid UTF-8")chained from theUnicodeDecodeErrorif the bytes are not UTF-8. OtherOSErrors propagate unchanged.
Solutions
1. Ask for a number. The answers are fed in automatically here, so each message appears on the same line as the prompt. When you type the answers yourself, each message starts on a new line.
2. Validation error.
Because ValidationError inherits from ValueError, callers that already catch ValueError keep working.
3. Retry with backoff.
Notice the bare raise on the last attempt: the caller sees the real ConnectionError with its original traceback. Errors outside retry_on, such as a TypeError from a bug, are never retried.
4. Safe file read.