Chapter 12 of 24 · Python for Beginners — Complete Free Tutorial
Error Handling with try/except
Every program you have written so far in the Python for Beginners series has assumed the data cooperates: numbers are numbers, keys exist, nobody divides by zero. Real input is not like that. Python reports trouble by raising an exception — an object that unwinds the stack until something handles it. This chapter explains the traceback, the four-clause try statement, how to catch the exceptions you actually expect, and how to raise, chain and define your own.
Exceptions are objects
When a program fails, Python builds an exception object, prints a traceback — the trail of frames from the failing line back up to the top — and stops. This is a complete example:
totals = {"north": 3}
print(totals["south"])
Expected output: (the file path is shortened to report.py)
Traceback (most recent call last):
File "report.py", line 2, in <module>
print(totals["south"])
~~~~~~^^^^^^^^^
KeyError: 'south'
Read it bottom-up: the last line names the exception class and its message, the line above it points at the exact expression with a squiggle underline, and the frames above show how you got there. Every exception is an instance of a class derived from BaseException, which means you can inspect it, catch it by type, or raise it yourself. Three details worth knowing now: the traceback goes to standard error and the program exits with status code 1; a message in single quotes such as KeyError: 'south' is the repr() of a value; and the caret marks the exact span of the failing expression, even when the failure happens inside a C function such as int().
try, except, else and finally
The try statement has four parts, and each has a distinct job:
try— the code that might fail.except— runs when a matching exception is raised; the program continues.else— runs only iftrycompleted without raising.finally— always runs, whether the body succeeded, failed or returned.
def read_number(text):
try:
value = int(text)
except ValueError:
print("not a number")
else:
print("parsed", value)
finally:
print("done")
read_number("7")
read_number("x")
Expected output:
parsed 7
done
not a number
done
Notice the order: on success the else branch runs before finally; on failure except runs and else is skipped, but finally happens either way. That last property makes finally the right home for cleanup — closing a file, releasing a lock — and chapter 15 replaces it with a with statement the moment you open a file.
Catch the exception you expect
Each failure has its own class, and the honest approach is to catch the ones you predicted and let the rest escape. These are the four you will meet most, with their exact messages on Python 3.12 and later — bind the object with except ValueError as err: when you need to report that message yourself:
| Exception | Raised when | Exact message |
|---|---|---|
ValueError | int("forty-two") | invalid literal for int() with base 10: 'forty-two' |
KeyError | totals["south"] on a missing key | 'south' — the repr of the key |
ZeroDivisionError | 10 / 0 | division by zero |
TypeError | 10 + "1" | unsupported operand type(s) for +: 'int' and 'str' |
Bare except: catches everything, including KeyboardInterrupt (Ctrl+C) and SystemExit (a clean shutdown) — so a script wrapped in one becomes impossible to interrupt, and the real bug disappears behind a handler that did not expect it. Catch specific classes, or at most Exception, and never except: pass. Type each expression from the table into the REPL yourself: the final line Python prints never varies.
Raising your own, and chaining the cause
raise builds and throws an exception yourself. It is how a function says “this input is not my problem to fix — it is yours”. When a low-level failure becomes a high-level one, from records the relationship:
def load_port():
config = {}
try:
return int(config["port"])
except KeyError as err:
raise RuntimeError("invalid configuration") from err
try:
load_port()
except RuntimeError as err:
print("caught:", err)
print("cause:", repr(err.__cause__))
Expected output:
caught: invalid configuration
cause: KeyError('port')
The resulting traceback prints two sections joined by the line The above exception was the direct cause of the following exception:, so the original KeyError is still visible. Use from None instead when the inner error is noise you have already translated — the validation example below does exactly that.
Custom exception classes
A new exception is a class with a docstring and nothing else. Deriving from Exception (never from BaseException) makes it catchable by every ordinary handler:
class InsufficientStock(Exception):
"""Raised when an order asks for more units than we hold."""
def reserve(stock, wanted):
if wanted > stock:
raise InsufficientStock(f"only {stock} left, asked for {wanted}")
return stock - wanted
try:
reserve(3, 10)
except InsufficientStock as err:
print("cannot fulfil order:", err)
Expected output:
cannot fulfil order: only 3 left, asked for 10
Naming your own domain errors pays off immediately: callers can catch InsufficientStock precisely, while an unrelated ValueError still propagates. The same discipline appears in TypeScript's approach in error handling and result types, and plain try/catch in error handling and debugging — the shape is the same, only Python lets you raise by class name with no constructor ceremony.
A worked program: validating input
Everything from this chapter in one realistic function: convert, validate, raise a clear message, and let the caller decide what a failure means:
def parse_age(raw):
try:
age = int(raw)
except ValueError:
raise ValueError(f"{raw!r} is not a whole number") from None
if age < 0 or age > 130:
raise ValueError(f"{age} is not a realistic age")
return age
for candidate in ("42", "forty", "-3"):
try:
print(parse_age(candidate), "years")
except ValueError as err:
print(f"rejected: {err}")
Expected output:
42 years
rejected: 'forty' is not a whole number
rejected: -3 is not a realistic age
Three habits in one short function: the except catches only ValueError, the raised message adds context the original lacked, and from None hides the internal int() failure that the caller never cared about. The rule that ties it together — handle a failure only if you can do something about it. Otherwise let it propagate, or wrap it with raise ... from err so nothing is lost.
Common mistakes
- Catching everything with a bare
except— it swallows Ctrl+C,SystemExitand bugs you have not imagined; catch specific classes. - Handling an error you cannot handle —
except: passturns a loud bug into a silent wrong answer; re-raise if there is nothing useful to do. - Catching
Exceptionaround a whole function — the narrower thetryblock, the clearer which line failed; wrap the risky expression only. - Reporting the wrong message — a
KeyErrorprints the missing key in quotes and nothing else; include the key and the operation yourself when you re-raise. - Using exceptions for ordinary flow — raising to skip an item in a hot loop is slow and hard to read; branch with
ifwhen no failure occurred.
Practice checklist
- Trigger each of the four exceptions from the table, read the traceback, and write down the exact final line Python printed.
- Add an
elseand afinallyto theread_numberexample and confirm the printed order for both inputs. - Rewrite a piece of code with a bare
exceptso it catches only the exceptions it actually expects. - Define your own exception for an invalid email address, raise it, and catch it by name at the call site.
- Take an input-reading program you wrote earlier, validate each value with a function like
parse_age, and print a friendly rejection for every bad entry.
What's next?
You can now catch problems deliberately instead of being surprised by them. Chapter 13 scales that discipline up to whole files of code in Modules, Packages and pip.
Frequently Asked Questions
Python is a general-purpose language first released in 1991, designed around one idea: code is read far more often than it is written. English keywords and indentation instead of braces make it readable, the standard library covers files, JSON, dates and testing, and pip adds thousands more packages. The same language handles scripts, web APIs, data work and machine learning.
None at all. Python is widely considered the friendliest first language: no declaration keywords, no braces, no build step, and error messages that name the problem. This series begins at your first print() call. If you already read JavaScript or TypeScript you will adjust quickly, but no previous language is assumed anywhere in these 24 chapters.
Python 3 only, any release from 3.12 onwards. Python 2 reached end of life in 2021 and its print "hi" statements and xrange() no longer run. One historical quirk remains: installers ship the interpreter as python3 on macOS and Linux, while Windows usually offers python or the py launcher, and every command in this series works with that one-word swap.
More than you might expect. Everyday automation such as renaming files, cleaning spreadsheets or scraping pages; web backends with Django, Flask or FastAPI; data analysis and machine learning with pandas, NumPy and PyTorch; DevOps tooling, desktop apps and games. Python is also the classic glue language, calling C, Rust or Fortran libraries from short readable scripts.
Pure Python can run 10 to 100 times slower than C, yet that rarely blocks you: heavy numeric work lives in C extensions such as NumPy, which Python coordinates. Dynamic typing does push errors to runtime, surfacing as a TypeError on the line that finally uses the bad value. Type hints checked by mypy move those mistakes back into your editor.
Only the interpreter. Install CPython from python.org, version 3.12 or newer, confirm it with python3 --version, and open a plain text editor; VS Code with the Python extension is the recommendation here. The early chapters use no third-party packages, so there is nothing else to set up until virtual environments and pip arrive.
A dict is a real runtime mapping: keys may be any hashable value rather than only strings, insertion order has been guaranteed since Python 3.7, and a missing key raises KeyError unless you ask with get(). JavaScript objects coerce every key to a string and carry a prototype chain. A TypeScript interface is erased at compile time and describes shape only; TypedDict is Python's checked equivalent.
Reach for a list when items will be added, removed or reordered: it is ordered, mutable and rich with methods. Pick a tuple for fixed records and function returns, because it is immutable and hashable, so it can be a dictionary key. Choose a set for unique values and fast membership checks; it has no order and de-duplicates on creation.
Yes, gradually. Annotate public function signatures first; the interpreter ignores hints at runtime, so a checker such as mypy is what gives them teeth. Add tests the same way: pytest collects any test_*.py file and plain assert statements, and a suite that finishes in a tenth of a second is one you will run after every change. Both habits cost little now and much more later.
JavaScript and TypeScript keep the browser, so anything rendered to the user stays theirs. Python takes the server: Django for full-featured applications, Flask for small ones, FastAPI for typed async APIs where your annotations become automatic documentation. Your TypeScript habits transfer directly, and asyncio mirrors the event loop you already know, just with different keywords.