Errors & Exceptions

Python · Lesson 30 of 56 8 min read Updated

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:

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

Text
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:

ExceptionTypical cause
ValueErrorRight type, wrong value, such as int("ninety")
TypeErrorWrong type, such as "Age: " + 21
ZeroDivisionErrorDividing by zero
KeyErrorA missing dictionary key
IndexErrorA list position that does not exist
FileNotFoundErrorOpening a file that is not there
NameErrorUsing 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.

PythonRun it yourself
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")
Output
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.

PythonRun it yourself
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"))
Output
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.

PythonRun it yourself
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")
Output
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.

PythonRun it yourself
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)
Output
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.

PythonRun it yourself
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__))
Output
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.

PythonRun it yourself
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])
Output
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: or except Exception: with pass hides real bugs, even typos. Catch the specific exceptions you expect.
  • Putting too much in try. If ten lines sit in try, 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 except block 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.
  • try holds risky code, except SomeError handles it, else runs if nothing failed and finally always runs.
  • Catch specific exceptions, and use as error to read the message.
  • Use raise to signal problems from your own functions, and raise ... from error to chain a clearer exception to the original.
  • Custom exceptions are classes that inherit from Exception.
  • ExceptionGroup with except* handles several errors at once; Python 3.14 allows except A, B: without brackets when there is no as.

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
Python
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}")
Output
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?

2. What does int("abc") raise?

3. What does raise StockError("...") from error do?

4. Which line is valid in Python 3.14?

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