Errors & Exceptions
Programs meet problems all the time: a user types "ninety" instead of 90, a file is missing, a network goes down. When Python hits a problem it cannot handle, it creates an exception, an object that describes what went wrong, and stops the program. With try and except you can catch exceptions and respond sensibly instead of crashing.
In this lesson you will catch and raise exceptions, clean up with finally, create your own exception types, and see the newer except* and bracket-free except syntax.
What an exception looks like
Suppose a program converts marks typed by a teacher into a number:
marks = int("ninety")Python cannot turn the word into a number, so it stops and prints a traceback: a report of where the error happened, ending with the exception type and message.
Traceback (most recent call last):
File "marks.py", line 1, in <module>
marks = int("ninety")
ValueError: invalid literal for int() with base 10: 'ninety'Always read a traceback from the bottom up. The last line tells you what went wrong (ValueError) and the lines above tell you where. These are the exceptions you will meet most often:
| Exception | Typical cause |
|---|---|
ValueError | Right type, wrong value, such as int("ninety") |
TypeError | Wrong type, such as "Age: " + 21 |
ZeroDivisionError | Dividing by zero |
KeyError | A missing dictionary key |
IndexError | A list position that does not exist |
FileNotFoundError | Opening a file that is not there |
NameError | Using a variable that was never created |
Catching exceptions with try and except
Put the risky code in a try block. If an exception happens there, Python jumps to the matching except block instead of crashing, and the program carries on.
entries = ["78", "ninety", "65", ""]
for text in entries:
try:
marks = int(text)
print("Saved:", marks)
except ValueError:
print(f"Skipped {text!r}: not a whole number")
print("Done")Saved: 78 Skipped 'ninety': not a whole number Saved: 65 Skipped '': not a whole number Done
Name the exact exception you expect. Writing except ValueError catches only that problem, so a different bug, such as a typo in a variable name, still shows up clearly.
Several exceptions and the error object
A try can have several except blocks, checked from top to bottom. Add as name to get the exception object, whose text explains the problem.
def share_bill(total, people):
try:
each = int(total) / int(people)
except ValueError as error:
return f"Bad number: {error}"
except ZeroDivisionError:
return "There must be at least one person"
return f"Each pays ₹{each:.2f}"
print(share_bill("1200", "4"))
print(share_bill("1200", "0"))
print(share_bill("12OO", "4"))Each pays ₹300.00 There must be at least one person Bad number: invalid literal for int() with base 10: '12OO'
To handle several types the same way, list them in brackets: except (ValueError, ZeroDivisionError) as error:.
else and finally
A try statement can have two more parts. The else block runs only if no exception happened. The finally block runs always, whether there was an error or not, so it is the place for clean-up such as closing a connection or printing a final message.
def read_age(text):
try:
age = int(text)
except ValueError:
print(f"{text!r} is not an age")
else:
print(f"Age saved: {age}")
finally:
print("Form checked")
read_age("21")
read_age("twenty")Age saved: 21 Form checked 'twenty' is not an age Form checked
Keep the try block small: only the line that can fail goes inside, and the code that depends on its success goes in else.
Raising your own exceptions
Your own functions can signal problems with raise. Choose a built-in exception that fits, and write a clear message.
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > balance:
raise ValueError(f"Cannot withdraw ₹{amount}; balance is ₹{balance}")
return balance - amount
print(withdraw(5000, 1500))
try:
withdraw(5000, 8000)
except ValueError as error:
print("Error:", error)3500 Error: Cannot withdraw ₹8000; balance is ₹5000
Raising an exception is better than returning a special value such as -1, because the caller cannot accidentally ignore it.
Custom exceptions and raise ... from
For your own programs you can define exception classes. A custom exception is a class that inherits from Exception (you will learn classes in Classes & Objects; for now, copy the pattern). It lets callers catch exactly your error.
Sometimes you catch a low-level error and raise a clearer one. Write raise NewError(...) from error to chain them: the new exception remembers the original one as its cause, and the traceback shows both.
class StockError(Exception):
"""Raised when an item cannot be supplied."""
stock = {"pen": 40, "notebook": 0}
def take(item, quantity):
try:
available = stock[item]
except KeyError as error:
raise StockError(f"We do not sell {item!r}") from error
if quantity > available:
raise StockError(f"Only {available} {item}(s) left")
stock[item] = available - quantity
for item, qty in [("pen", 5), ("notebook", 2), ("eraser", 1)]:
try:
take(item, qty)
print(f"Packed {qty} {item}(s)")
except StockError as error:
print("Sorry:", error, "| cause:", repr(error.__cause__))Packed 5 pen(s)
Sorry: Only 0 notebook(s) left | cause: None
Sorry: We do not sell 'eraser' | cause: KeyError('eraser')Inside an except block, a bare raise with nothing after it re-raises the current exception unchanged, which is useful when you only want to log a problem and let it continue upwards.
Exception groups
Code that does several jobs at once, such as validating every field of a form or running tasks with asyncio, may fail in more than one way at the same time. An ExceptionGroup bundles several exceptions together, and except* handles each type inside the group.
try:
raise ExceptionGroup("form errors", [
ValueError("age must be a number"),
TypeError("name is missing"),
ValueError("PIN code must have 6 digits"),
])
except* ValueError as group:
for error in group.exceptions:
print("Value problem:", error)
except* TypeError as group:
print("Type problem:", group.exceptions[0])Value problem: age must be a number Value problem: PIN code must have 6 digits Type problem: name is missing
Unlike normal except, more than one except* block can run, one for each matching type. You will not need this often as a beginner, but now you will recognise it.
Common mistakes
- Catching everything. A bare
except:orexcept Exception:withpasshides real bugs, even typos. Catch the specific exceptions you expect. - Putting too much in
try. If ten lines sit intry, you cannot tell which one failed. Keep only the risky line there. - Swallowing errors silently. If you catch an exception, tell the user or log it; an empty
exceptblock makes debugging very hard. - Reading the traceback from the top. The useful line is the last one, with the exception type and message.
Key points
- An exception is an object Python creates when something goes wrong; uncaught, it stops the program with a traceback.
tryholds risky code,except SomeErrorhandles it,elseruns if nothing failed andfinallyalways runs.- Catch specific exceptions, and use
as errorto read the message. - Use
raiseto signal problems from your own functions, andraise ... from errorto chain a clearer exception to the original. - Custom exceptions are classes that inherit from
Exception. ExceptionGroupwithexcept*handles several errors at once; Python 3.14 allowsexcept A, B:without brackets when there is noas.
Exercise
Write a function ticket_fare(distance_text) for a bus company in Jaipur. It should convert distance_text to a float, raise a ValueError with a clear message if the distance is zero or negative, and return a fare of ₹8 per km with a minimum of ₹20. Then call it in a loop for "12", "2", "-5" and "far", printing either the fare or the error message.
Show solution
def ticket_fare(distance_text):
distance = float(distance_text)
if distance <= 0:
raise ValueError(f"Distance must be positive, got {distance}")
return max(20, distance * 8)
for text in ["12", "2", "-5", "far"]:
try:
print(f"{text} km: ₹{ticket_fare(text):.0f}")
except ValueError as error:
print(f"{text!r}: {error}")12 km: ₹96 2 km: ₹20 '-5': Distance must be positive, got -5.0 'far': could not convert string to float: 'far'
Both problems arrive as ValueError: one raised by float() itself and one raised by our own check. The loop handles them with a single except block.
Quick Quiz
1. When does a finally block run?
finally is for clean-up code that must run in every case.
2. What does int("abc") raise?
The argument is a string, which is the right type, but its value cannot be converted.
3. What does raise StockError("...") from error do?
The from clause chains exceptions, so the traceback shows both and __cause__ holds the original.
4. Which line is valid in Python 3.14?
PEP 758 allows brackets to be left out only when there is no as clause.
Learn Python with a mentor
Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
