All 57 lessons
Lesson 23 of 57 · Files, Errors and Modules
Python Errors and Exceptions: try, except, finally and raise
By Skillancy Editorial Team
Run the code in the free Python Compiler or on your own computer. Lessons that read files use the FreshBasket practice data: download the dataset (ZIP).
Every program fails sometimes. A file is missing, a user types "abc" where a number was expected, a network call times out, a CSV has an empty cell where you assumed a value. The question is never whether errors will happen, but what your program does when they do. A script that crashes halfway through a 2-hour data load and loses everything is a poor script. A script that notices the bad row, logs it, and carries on is a professional one.
Python reports problems as exceptions. In this lesson you will learn to read the traceback (the error message that scares beginners but is actually your best debugging friend), catch specific exceptions with try/except, guarantee cleanup with finally, and signal problems yourself with raise. We finish by building a validation function and running it over the messy FreshBasket orders file, a realistic example of defensive programming.
Worked examples
Example 1: Reading a traceback
Question: What does an error message tell you?
def parse_minutes(text):
return int(text)
def average_minutes(values):
return sum(parse_minutes(v) for v in values) / len(values)
average_minutes(["12", "35", "n/a"])Output
Traceback (most recent call last):
File "example.py", line 7, in <module>
average_minutes(["12", "35", "n/a"])
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 5, in average_minutes
return sum(parse_minutes(v) for v in values) / len(values)
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "example.py", line 5, in <genexpr>
return sum(parse_minutes(v) for v in values) / len(values)
~~~~~~~~~~~~~^^^
File "example.py", line 2, in parse_minutes
return int(text)
ValueError: invalid literal for int() with base 10: 'n/a'How it works: Read a traceback from the bottom. The last line names the exception type and message (ValueError: invalid literal for int()). The lines above show the chain of calls that led there, from the outermost call down to the exact line that failed. Here the root cause is inside parse_minutes, though the call started in average_minutes.
Reading the result: The bad value was the string n/a, which cannot become an integer. Fix the data or handle the error where the conversion happens.
Example 2: The usual suspects
Question: Which exceptions will you see most often?
def attempt(label, func):
try:
func()
except Exception as e:
print(f"{label:<18}{type(e).__name__}: {e}")
attempt("bad conversion", lambda: int("abc"))
attempt("missing key", lambda: {"a": 1}["b"])
attempt("bad index", lambda: [1, 2, 3][10])
attempt("divide by zero", lambda: 10 / 0)
attempt("wrong type", lambda: "5" + 5)
attempt("missing attribute", lambda: "text".push)
attempt("unknown name", lambda: undefined_name)
attempt("missing file", lambda: open("nope.csv"))Output
bad conversion ValueError: invalid literal for int() with base 10: 'abc'
missing key KeyError: 'b'
bad index IndexError: list index out of range
divide by zero ZeroDivisionError: division by zero
wrong type TypeError: can only concatenate str (not "int") to str
missing attribute AttributeError: 'str' object has no attribute 'push'
unknown name NameError: name 'undefined_name' is not defined
missing file FileNotFoundError: [Errno 2] No such file or directory: 'nope.csv'How it works: The helper runs each lambda and prints the exception's class name and message. These eight are the ones you will meet daily: ValueError, KeyError, IndexError, ZeroDivisionError, TypeError, AttributeError, NameError and FileNotFoundError. except Exception as e catches any ordinary error and gives it the name e.
Reading the result: Each line named a different class. Learning the name tells you the likely cause instantly: a KeyError means a dictionary key problem, a TypeError means mixing incompatible types.
Example 3: try and except
Question: Convert user input safely, with a fallback.
def to_int(text, default=None):
try:
return int(text)
except ValueError:
return default
print(to_int("42"))
print(to_int("4x2"))
print(to_int("4x2", default=0))
print([to_int(x, 0) for x in ["12", "", "7", "NA"]])Output
42
None
0
[12, 0, 7, 0]How it works: Code in the try block runs normally. If it raises a ValueError, Python jumps to the matching except block instead of crashing. Execution then continues after the whole statement. Catching ValueError specifically means other unexpected errors will still surface instead of being hidden.
Reading the result: The valid text gave 42, the invalid one gave None, and the list comprehension cleaned four raw values into [12, 0, 7, 0].
Example 4: Several except clauses
Question: Handle different failures differently.
def safe_ratio(data, key_a, key_b):
try:
return data[key_a] / data[key_b]
except KeyError as e:
return f"missing field {e}"
except ZeroDivisionError:
return "denominator is zero"
except TypeError:
return "non-numeric value"
row = {"revenue": 900, "orders": 30, "refunds": 0, "note": "n/a"}
print(safe_ratio(row, "revenue", "orders"))
print(safe_ratio(row, "revenue", "cost"))
print(safe_ratio(row, "revenue", "refunds"))
print(safe_ratio(row, "revenue", "note"))
print(safe_ratio({"a": 1, "b": 2}, "a", "b"))
try:
int("x")
except (ValueError, TypeError) as e:
print("either:", type(e).__name__)Output
30.0
missing field 'cost'
denominator is zero
non-numeric value
0.5
either: ValueErrorHow it works: Python checks the except clauses top to bottom and runs the first that matches. A tuple catches several types with one handler. Order matters when classes are related (a more specific class must come before a general one).
Reading the result: Four calls hit four different outcomes: 30.0 for the normal case, then a message for a missing key, a zero denominator and a non-numeric field.
Example 5: else and finally
Question: Run code only on success, and always clean up.
def load_count(path):
f = None
try:
f = open(path, encoding="utf-8")
except FileNotFoundError:
print("no such file:", path)
return None
else:
print("opened fine")
return sum(1 for _ in f)
finally:
print("cleanup runs always")
if f:
f.close()
print(load_count("feedback.txt"))
print(load_count("missing.txt"))Output
opened fine
cleanup runs always
40
no such file: missing.txt
cleanup runs always
NoneHow it works: The else block runs only if the try raised nothing. The finally block runs no matter what: after success, after a handled error, even after a return. It is where you release resources. For files, a with statement does this for you, but finally is the general tool for connections, locks and temporary files.
Reading the result: For the real file the function returned 40 lines, and for the missing one None; "cleanup runs always" was printed both times.
Example 6: Raising exceptions
Question: Reject invalid input in your own function.
def apply_discount(price, pct):
if not isinstance(price, (int, float)):
raise TypeError("price must be a number")
if price < 0:
raise ValueError(f"price cannot be negative: {price}")
if not 0 <= pct <= 100:
raise ValueError(f"discount must be between 0 and 100, got {pct}")
return price * (1 - pct / 100)
print(apply_discount(200, 15))
for args in [(-5, 10), (200, 150), ("200", 10)]:
try:
apply_discount(*args)
except (ValueError, TypeError) as e:
print(type(e).__name__, "-", e)Output
170.0
ValueError - price cannot be negative: -5
ValueError - discount must be between 0 and 100, got 150
TypeError - price must be a numberHow it works: raise ExceptionType("message") stops the function immediately and hands the problem to the caller. Validating inputs at the start and failing loudly with a clear message is far better than silently returning a nonsense value. Choose the built-in class that best describes the problem.
Reading the result: A valid call gave 170.0, and the three invalid calls were each rejected with a different message.
Example 7: Custom exceptions
Question: Create an exception type specific to your domain.
class OrderError(Exception):
"""Base class for order problems."""
class InvalidStatus(OrderError):
def __init__(self, status):
super().__init__(f"unknown status: {status!r}")
self.status = status
VALID = {"Delivered", "Cancelled", "Returned"}
def check_status(status):
if status not in VALID:
raise InvalidStatus(status)
return status
for s in ["Delivered", "Shipped"]:
try:
print(check_status(s))
except OrderError as e:
print("caught:", e, "| status =", e.status)
print(issubclass(InvalidStatus, Exception))Output
Delivered
caught: unknown status: 'Shipped' | status = Shipped
TrueHow it works: A custom exception is a class that inherits from Exception (classes are covered in two lessons). Making a base class (OrderError) lets callers catch every order-related problem with one except, or a specific child. Extra attributes carry details for the handler.
Reading the result: Delivered passed, while Shipped raised an InvalidStatus whose status attribute was Shipped. Custom exceptions make large programs easier to debug.
Example 8: Re-raising and chaining
Question: Add context to an error without hiding the original.
def load_price(raw):
try:
return float(raw)
except ValueError as e:
raise ValueError(f"bad price in catalogue: {raw!r}") from e
try:
load_price("12.5.1")
except ValueError as e:
print(e)
print("caused by:", repr(e.__cause__))
def noisy():
try:
1 / 0
except ZeroDivisionError:
print("logging the failure...")
raise
try:
noisy()
except ZeroDivisionError:
print("still propagated")Output
bad price in catalogue: '12.5.1'
caused by: ValueError("could not convert string to float: '12.5.1'")
logging the failure...
still propagatedHow it works: raise NewError(...) from e links the new exception to the original, so the traceback shows both. A bare raise inside an except block re-raises the current exception after you have logged it. Use this pattern when you want to record a problem but let a higher level decide what to do.
Reading the result: The wrapped message was bad price in catalogue: '12.5.1' and the original ValueError is kept in __cause__. The second function logged and re-raised, so the outer handler still ran.
Example 9: assert
Question: How do you check your own assumptions?
def mean(values):
assert len(values) > 0, "mean of an empty list"
return sum(values) / len(values)
print(mean([4, 6]))
try:
mean([])
except AssertionError as e:
print("AssertionError:", e)Output
5.0
AssertionError: mean of an empty listHow it works: assert condition, message raises AssertionError if the condition is false. Use it for conditions that indicate a programming bug ("this should never happen"), not for validating user input, because assertions can be switched off when Python runs with -O. For anything the outside world controls, raise a proper exception.
Reading the result: The valid call gave 5.0; the empty list triggered the assertion with its message.
Example 10: Validate messy data without stopping
Question: Go through messy_orders.csv, collect good rows, and report each bad one with a reason.
import csv
good, bad = [], []
with open("messy_orders.csv", newline="", encoding="utf-8") as f:
for line_no, row in enumerate(csv.DictReader(f), start=2):
try:
amount = float(row["amount"])
if amount <= 0:
raise ValueError("non-positive amount")
if not row["city"].strip():
raise ValueError("blank city")
good.append({**row, "amount": amount})
except ValueError as e:
bad.append((line_no, row["order_id"], str(e)))
print(len(good), len(bad))
for item in bad[:4]:
print(item)
from collections import Counter
print(Counter(reason.split(":")[0][:25] for _, _, reason in bad).most_common(3))Output
184 254
(3, 'M0028', "could not convert string to float: 'NA'")
(5, 'M0156', "could not convert string to float: 'NA'")
(8, 'M0115', "could not convert string to float: '1,569'")
(9, 'M0239', "could not convert string to float: '₹1,827.03'")
[('could not convert string ', 254)]How it works: Each row is validated inside its own try, so one bad row is recorded in bad with its line number and reason, and the loop moves on. We both raise our own ValueError for business rules and let float() raise on text such as NA. At the end we have two lists: usable data, and an audit trail of problems.
Reading the result: Of 438 rows, 184 passed validation and 254 were rejected. This pattern is the core of every data-cleaning script, and the pandas lesson will show a vectorised version.
Common mistakes
1. A bare except: hides every error, including typing mistakes and Ctrl+C.
try:
result = undefined_variable + 1
except:
print("something went wrong") # but what? you will never knowOutput
something went wrong2. Catching Exception and doing nothing (pass) so that bugs vanish silently.
3. Putting too much code in try. Keep it to the lines that can actually fail.
4. Using exceptions for normal flow when an if is clearer (checking a dictionary key with in).
5. Forgetting that finally runs even after return.
Practice questions
Practice 1 (Easy): Safe division
Write safe_div(a, b) returning None when b is zero. Test with (10, 2) and (1, 0).
Show solution
def safe_div(a, b):
try:
return a / b
except ZeroDivisionError:
return None
print(safe_div(10, 2), safe_div(1, 0))Output
5.0 NoneOnly the expected exception is caught. The results are 5.0 and None.
Practice 2 (Medium): Parse a list of ratings
Write parse_ratings(texts) that converts strings to integers 1 to 5, returning the valid ratings and a list of the rejected texts. Test with ["5", "3", "x", "9", "1", ""].
Show solution
def parse_ratings(texts):
ok, rejected = [], []
for t in texts:
try:
r = int(t)
if not 1 <= r <= 5:
raise ValueError
ok.append(r)
except ValueError:
rejected.append(t)
return ok, rejected
ok, rejected = parse_ratings(["5", "3", "x", "9", "1", ""])
print(ok, rejected)Output
[5, 3, 1] ['x', '9', '']The range check reuses the same handler by raising ValueError. We kept 3 ratings and rejected 3 values.
Practice 3 (Hard): A retry helper
Write retry(func, attempts) that calls func() up to attempts times, returning the first successful result and re-raising the last error if all fail. Test it with a function that fails twice and then works.
Show solution
def retry(func, attempts=3):
last = None
for i in range(1, attempts + 1):
try:
return func()
except Exception as e:
last = e
print(f"attempt {i} failed: {e}")
raise last
calls = {"n": 0}
def flaky():
calls["n"] += 1
if calls["n"] < 3:
raise ConnectionError("temporary failure")
return "ok"
print(retry(flaky, 5))Output
attempt 1 failed: temporary failure
attempt 2 failed: temporary failure
okThe helper hides temporary failures but reports the final one. It succeeded on call number 3. The API lesson will use the same idea against the flaky mock endpoint.
Key takeaways
- Read tracebacks from the bottom: the last line gives the type and message.
- Catch specific exceptions; never use a bare
except:. elseruns on success andfinallyalways runs, making it the home for cleanup.raisesignals problems; custom exception classes make domain errors clear.- Validate inputs early and fail with helpful messages.
- Process messy data row by row, collecting bad rows with reasons rather than stopping at the first one.
Frequently asked questions
What is the difference between an error and an exception?
In everyday Python they mean the same. Technically, a SyntaxError happens before the program runs (when Python parses it), whereas exceptions happen during execution and can be caught.
Should I catch Exception or something specific?
Prefer the most specific type you expect. Catch Exception only at the outermost level (for example, to log a failure and exit cleanly).
What does finally do if there is no error?
It still runs, after the try (and else) block finishes. That is why it is the right place for cleanup.
Are exceptions slow?
Raising and catching one is slower than an if check, but cheap enough for normal use. Python style favours "ask forgiveness, not permission", while still avoiding try/except in hot loops where a simple test works.
How do I see the traceback of a caught error?
Use import traceback; traceback.print_exc() inside the except block, or the logging module's logger.exception(...), which records the full traceback.
What is the difference between raise and raise e?
A bare raise re-raises the active exception with its original traceback. raise e also works but can add an extra line to the traceback. Use the bare form for re-raising.
Keep learning Python
One email a week with Python and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.
Ready to practise? Try the Python interview questions