Skip to content

Exception Handling

When something goes wrong at runtime — file missing, network down, divide by zero — Python raises an exception. If you don't handle it, your program crashes.

A crashing example

x = 10
y = 0
print(x / y)         # ZeroDivisionError
print("This never runs")

The program stops at the error. Everything after is unreachable.

try / except — catch and recover

try:
    x = 10
    y = 0
    print(x / y)
except ZeroDivisionError:
    print("Can't divide by zero!")

print("Program continues happily.")
  • try: block — risky code.
  • except SomeError: — what to do if that error happens.

Catching multiple errors

def safe_divide(a, b):
    try:
        return int(a) / int(b)
    except ZeroDivisionError:
        return "Error: division by zero"
    except ValueError:
        return "Error: inputs must be integers"
    except Exception as e:
        # Catch-all — keep last
        return f"Unknown error: {e}"

print(safe_divide(10, 2))      # 5.0
print(safe_divide(10, 0))      # division by zero
print(safe_divide("a", "b"))   # ValueError → "inputs must be integers"

You can catch several in one line as a tuple:

try:
    int("hello")
except (ValueError, TypeError) as e:
    print(f"Caught: {type(e).__name__}: {e}")

else and finally

  • else: runs if no exception was raised.
  • finally: runs either way — for cleanup.
def read_number(text):
    try:
        n = int(text)
    except ValueError:
        print(f"'{text}' is not a number")
    else:
        print(f"Got number: {n}")
    finally:
        print("done\n")

read_number("42")
read_number("hello")

finally is perfect for closing files, releasing locks, etc. — but using with (context managers) is usually cleaner.

Common built-in exceptions

Exception When it happens
ZeroDivisionError Dividing by zero
ValueError Right type, wrong value (int("abc"))
TypeError Wrong type ("a" + 5)
KeyError Dict key missing (d["missing"])
IndexError List index out of range
FileNotFoundError Trying to open a file that doesn't exist
PermissionError OS denied access
AttributeError obj.missing_method
ImportError / ModuleNotFoundError import failed
NameError Used a variable not defined
StopIteration Iterator exhausted
RecursionError Stack overflow
RuntimeError Generic catch-all for misc problems

Examples:

errors_demo = [
    ("ValueError",   lambda: int("hello")),
    ("TypeError",    lambda: "a" + 5),
    ("KeyError",     lambda: {"a": 1}["b"]),
    ("IndexError",   lambda: [1, 2, 3][10]),
    ("AttributeError", lambda: "hello".not_a_method()),
]

for name, fn in errors_demo:
    try:
        fn()
    except Exception as e:
        print(f"{name}: {type(e).__name__}{e}")

Raising exceptions yourself

def set_age(value):
    if value < 0:
        raise ValueError("age cannot be negative")
    if value > 150:
        raise ValueError("age too large to be realistic")
    return value

try:
    set_age(-5)
except ValueError as e:
    print("Caught:", e)

Custom exception classes

For complex apps, define your own — gives callers a meaningful type to catch.

class InsufficientFundsError(Exception):
    """Raised when a withdrawal exceeds balance."""

class Account:
    def __init__(self, owner, balance):
        self.owner, self.balance = owner, balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(
                f"{self.owner} has ₹{self.balance}; can't withdraw ₹{amount}"
            )
        self.balance -= amount

# Use it
acc = Account("Alice", 1000)
try:
    acc.withdraw(5000)
except InsufficientFundsError as e:
    print("⚠️", e)

try with file I/O

try:
    with open("does_not_exist.txt") as f:
        content = f.read()
except FileNotFoundError:
    print("File not found — using default content")
    content = "DEFAULT"

print(content)

try with multiple operations

def parse_score(text):
    try:
        score = int(text)
        if score < 0 or score > 100:
            raise ValueError(f"score {score} out of range 0..100")
        return score
    except ValueError as e:
        print(f"Bad score '{text}': {e}")
        return None

inputs = ["85", "120", "abc", "50"]
for s in inputs:
    print(f"{s!r:>6}{parse_score(s)}")

Best practices

  1. Catch specific exceptionsexcept ValueError: not except:. A bare except catches everything including KeyboardInterrupt, masking real bugs.
  2. Don't swallow errors silently — at least log them.
  3. finally for cleanup — but with is usually cleaner.
  4. Fail fast — raise as soon as you detect a problem. Don't continue with bad data.
  5. Don't use exceptions for control flowif/else is clearer for normal logic.

Bad — too broad:

try:
    do_something()
except:                  # catches EVERYTHING, including bugs
    pass                  # silent failure — debugging nightmare

Good — specific + informative:

try:
    do_something()
except ValueError as e:
    logger.warning("bad value: %s", e)
    raise                # re-raise so caller knows

Mini-project — robust file reader

def read_config(path):
    try:
        with open(path) as f:
            import json
            return json.load(f)
    except FileNotFoundError:
        print(f"Config '{path}' not found — using defaults.")
        return {"theme": "light", "lang": "en"}
    except json.JSONDecodeError as e:
        print(f"Config '{path}' is corrupt: {e}")
        return None
    except PermissionError:
        print(f"No permission to read '{path}'.")
        return None

# Create a corrupt config
with open("bad-config.json", "w") as f:
    f.write("{not valid JSON}")

print(read_config("missing.json"))
print(read_config("bad-config.json"))

Practice

What does this print?

Expected: caughtcleanup

try:
    int("abc")
except ValueError:
    print("caught", end="")
finally:
    print("cleanup")

Catch the specific error, not a bare except

Expected: division by zero

try:
    x = 1 / 0
except:                       # bug: bare except swallows EVERYTHING
    print("error")

Quiz — Quick check

What you remember

Q1. Which exception is raised by int("hello")?

  • TypeError
  • ValueError
  • NameError
  • SyntaxError

Why: The argument is the right type (str) but the wrong valueint() can't parse "hello". TypeError is for wrong-type situations like "a" + 5.

Q2. When does the finally block run?

  • Only if the try block succeeded
  • Only if an exception was caught
  • Always — whether the try succeeded, raised, or even returned early
  • Never — it's deprecated

Why: finally is for cleanup code that must run no matter what. Closing files, releasing locks, restoring state.

Q3. What's wrong with except: (a bare except)?

  • It's slow
  • It catches every exception, including system signals like Ctrl+C, hiding real bugs
  • It's a syntax error
  • Nothing — it's recommended

Why: Bare except: catches KeyboardInterrupt, SystemExit, and any unforeseen error — making it nearly impossible to debug or even kill the program cleanly. Always catch specific exceptions.

Common doubts

When should I raise instead of return None?

Use raise when something is unexpectedly wrong — invalid input, missing file, broken contract. Use return None (or a sentinel) when "nothing found" is a normal outcome — dict.get(key), parsing optional fields. Exceptions are for failures; return values are for results.

Should I create custom exception classes?

Yes, for non-trivial apps. Custom exceptions let callers catch your errors specifically: except InsufficientFundsError: is clearer than except ValueError: and won't accidentally catch unrelated value errors. Subclass Exception (not BaseException).

Is it bad to catch exceptions just to ignore them?

Almost always yes. "Swallowing" exceptions hides bugs. At minimum, log them. The rare valid case: a non-critical operation (e.g. updating a cache) where failure should not stop the main flow — but even then, log the exception with logging.exception() so it's not invisible.

What's next

Iterators, Generators & Decorators