Beginner · Lesson 30 of 94

Exceptions properly

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

Concept

An exception is Python's way of saying "this line cannot continue". Until now you have let exceptions end the program and read the traceback. In real programs, some failures are expected: a user types a word where a number belongs, a file is missing, a network request times out. try/except lets you name the failure you expect, handle it, and carry on. Everything you did not name still propagates, which is what you want: an unexpected error should be loud.

Exceptions properly: try: risky code → else: runs on success → except: handle it → propagates up to the caller → finally: always runs
Exceptions properly · Figure 1 · Open for full size

The single most important rule is to catch specific exceptions. except ValueError: says "I know what to do when the text is not a number". A bare except: or except Exception: catches everything, including bugs in your own code and the KeyboardInterrupt from Ctrl+C, and hides them. Since Python 3.14 you can list several types without parentheses, except ValueError, TypeError:, and except ValueError as err: gives you the exception object to inspect or log.

Keep the try block small: one operation that can fail, not the whole function. Code that depends on that operation succeeding goes in else, so an exception from it is not mistaken for the one you were catching. finally runs whether or not anything went wrong and even if the block returns, which makes it the place for cleanup; with is a shorter way to get the same guarantee for files and locks.

Clause Runs when Typical use
try always, first the one operation that might fail
except X an X (or subclass) was raised in try recover, retry, report
else try finished with no exception the follow-up that needs the result
finally always, last, even after return close, unlock, clean up

Your own code should raise exceptions too. When a function receives an argument it cannot work with, raise ValueError("amount must be positive") is the honest response; returning -1 or None and hoping the caller checks is how bugs hide. Pick the built-in type whose meaning matches: ValueError for a bad value, TypeError for a wrong type, KeyError or LookupError for a missing thing, RuntimeError when nothing more specific fits.

For a program of any size, define your own exception types. A class with a pass body that inherits from Exception is enough: class PaymentError(Exception): pass. Callers can then catch your program's failures without catching unrelated ones, and a small hierarchy, a base class with a few specific subclasses, lets them choose how precisely to catch. You will learn classes fully in a few lessons; this pattern is simple enough to use now.

Exceptions properly: Exception → AppError → ConfigError → PaymentError → CardDeclined → InsufficientFunds
Exceptions properly · Figure 2 · Open for full size

Code

Catching a specific failure and letting the program continue. Bad input is handled; the loop moves on; the summary still prints.

# catch.py
entries = ["12", "7", "twelve", "3.5", "40"]
total = 0
skipped = []

for entry in entries:
    try:
        total += int(entry)
    except ValueError:
        skipped.append(entry)

print("total:", total)
print("skipped:", skipped)
Output
total: 59
skipped: ['twelve', '3.5']

All four clauses together, with the exception object in hand. The first call succeeds and runs else; the second fails and runs except; finally runs both times.

# four_clauses.py
def read_number(text):
    try:
        value = int(text)
    except ValueError as err:
        print("  except:", err)
        return None
    else:
        print("  else: parsed", value)
        return value
    finally:
        print("  finally: done with", repr(text))

print(read_number("42"))
print(read_number("x"))
Output
  else: parsed 42
  finally: done with '42'
42
  except: invalid literal for int() with base 10: 'x'
  finally: done with 'x'
None

Raising your own, with the type that matches the problem. Callers that catch ValueError get a clear message; callers that do not get an honest crash instead of a wrong answer.

# raise_own.py
def withdraw(balance, amount):
    if not isinstance(amount, (int, float)):
        raise TypeError(f"amount must be a number, got {type(amount).__name__}")
    if amount <= 0:
        raise ValueError("amount must be positive")
    if amount > balance:
        raise ValueError(f"cannot withdraw {amount} from balance {balance}")
    return balance - amount

print(withdraw(100, 30))
try:
    withdraw(100, -5)
except ValueError as err:
    print("handled:", err)
print(withdraw(100, 500))
Output
70
handled: amount must be positive
Traceback (most recent call last):
  File "raise_own.py", line 15, in <module>
    print(withdraw(100, 500))
          ~~~~~~~~^^^^^^^^^^
  File "raise_own.py", line 7, in withdraw
    raise ValueError(f"cannot withdraw {amount} from balance {balance}")
ValueError: cannot withdraw 500 from balance 100

A small hierarchy, and the 3.14 form of catching several types at once. Catching the base class handles both specific errors; catching KeyError, IndexError together shows the parenthesis-free syntax.

# hierarchy.py
class PaymentError(Exception):
    pass

class CardDeclined(PaymentError):
    pass

class InsufficientFunds(PaymentError):
    pass

def charge(card, amount):
    if card == "0000":
        raise CardDeclined("card 0000 was declined")
    if amount > 50:
        raise InsufficientFunds(f"{amount} exceeds the limit")
    return "ok"

for card, amount in [("1234", 20), ("0000", 20), ("1234", 90)]:
    try:
        print(charge(card, amount))
    except PaymentError as err:
        print(f"{type(err).__name__}: {err}")

data = {"a": [1]}
try:
    print(data["a"][5])
except KeyError, IndexError:
    print("missing key or index")
Output
ok
CardDeclined: card 0000 was declined
InsufficientFunds: 90 exceeds the limit
missing key or index

Try it yourself

Create safe_input.py with a function ask_int(prompt) that keeps asking until the user types a valid integer, catching ValueError each time, and returns it. Then write divide(a, b) that raises a ValueError with a clear message when b is zero instead of letting ZeroDivisionError escape, and a small try/except/else/finally that calls it and prints which clauses ran.

Common mistakes

What's next

Your programs can fail gracefully. Next you will split them across files, with modules, packages, and imports.

Course overview