Part of Python for Beginners

Exception Handling (try, except, raise, finally, custom exceptions) - Python Tutorial #16

Sandy LaneSandy Lane

Video: Exception Handling (try, except, raise, finally, custom exceptions) - Python Tutorial #16 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Python Exception Handling: try, except, else, finally, raise

try: runs the risky code. except SomeError as e: handles a specific failure. else: runs if no exception. finally: runs no matter what. raise SomeError("message") to throw your own.

When something can fail at runtime — a missing key, bad input, network timeout — Python raises an exception. Handling them well is the difference between a script that crashes and a program that recovers.

try / except

try:
  result = 10 / 0
except ZeroDivisionError:
  print("Cannot divide by zero!")

try runs the protected block. If anything inside raises a ZeroDivisionError, control jumps to the except clause. Other exceptions propagate normally.

try:
  number = int("hello")
except ValueError:
  print("Invalid number format!")

Always catch the specific exception you expect. Bare except: catches everything — including KeyboardInterrupt and SystemExit, which you usually want to let through.

Multiple except clauses

items = [10, 0, "five"]
for item in items:
  try:
    result = 100 / int(item)
    print(f"100 / {item} = {result}")
  except ZeroDivisionError:
    print(f"{item}: cannot divide by zero")
  except ValueError:
    print(f"{item}: not a valid number")

Multiple except clauses handle different exception types. Python checks them top-to-bottom; the first matching one runs.

You can also catch multiple types in one clause:

try:
  ...
except (ValueError, TypeError) as e:
  print(f"Bad input: {e}")

Capturing the exception with as

try:
  data = [1, 2, 3]
  print(data[10])
except Exception as e:
  print(f"Error: {type(e).__name__}: {e}")
# Error: IndexError: list index out of range

except SomeError as e: binds the exception object to e. You can read its message (str(e)), its type (type(e).__name__), and any additional attributes the specific exception class provides.

else clause

for value in ["42", "abc"]:
  try:
    number = int(value)
  except ValueError:
    print(f"'{value}' is not a number")
  else:
    print(f"Converted '{value}' to {number}")

else runs only if no exception was raised. Use it for code that depends on the try block succeeding, but shouldn't itself be protected by the same except.

The pattern: keep the try block as small as possible. If int(value) succeeds, the rest of the work goes in else.

finally clause

try:
  print("Opening resource...")
  result = 10 / 2
except ZeroDivisionError:
  print("Division error!")
else:
  print(f"Result: {result}")
finally:
  print("Closing resource...")

finally runs always — whether or not an exception occurred, whether or not it was caught, even on return or break.

Use it for cleanup: closing files, releasing locks, restoring state. (Though with blocks usually handle this more cleanly.)

The full grammar

try:
  # risky code
except SpecificError as e:
  # handle specific
except (ErrorA, ErrorB):
  # handle multiple
except Exception:
  # catch everything else (rarely needed)
else:
  # ran only if try succeeded
finally:
  # always runs

You don't need all parts. Common combinations:

  • try/except — handle a known failure.
  • try/except/else — keep "if it worked" code separate.
  • try/finally — ensure cleanup.
  • try/except/finally — handle + cleanup.

Raising exceptions

def validate_age(age):
  if age < 0:
    raise ValueError("Age cannot be negative")
  if age > 150:
    raise ValueError("Age seems unrealistic")
  return f"Age {age} is valid"

try:
  validate_age(-5)
except ValueError as e:
  print(f"Bad age: {e}")
# Bad age: Age cannot be negative

raise SomeError("message") throws an exception. Builds a traceback automatically.

Use the most specific built-in exception that fits:

  • ValueError — value has the right type but wrong content.
  • TypeError — wrong type (e.g., len(5)).
  • KeyError — missing dict key.
  • IndexError — out-of-range index.
  • FileNotFoundError — file not found.
  • RuntimeError — generic, when nothing else fits.

Custom exception classes

class InvalidEmailError(ValueError):
  pass

def validate_email(email):
  if "@" not in email:
    raise InvalidEmailError(f"'{email}' is not a valid email")

try:
  validate_email("hello")
except InvalidEmailError as e:
  print(e)

Subclass Exception (or a more specific built-in) to create your own exception types. The class name is the documentation — InvalidEmailError is self-explanatory.

class APIError(Exception):
  def __init__(self, message, status_code):
    super().__init__(message)
    self.status_code = status_code

You can attach data to the exception. Useful for HTTP libraries, parsers, etc.

Re-raising

try:
  do_something()
except ValueError:
  log_error()
  raise    # re-raises the same exception

A bare raise (inside an except) re-raises the current exception. Useful when you want to log/cleanup but still let the error propagate.

To wrap and add context:

try:
  parse(data)
except ValueError as e:
  raise ParseError(f"Failed to parse: {data}") from e

raise X from Y chains exceptions — the original is shown as the "direct cause" in the traceback.

Nested try/except

data = {"users": ["Alice", "Bob"]}
try:
  users = data["users"]
  try:
    print(f"Third user: {users[2]}")
  except IndexError:
    print("No third user found")
except KeyError:
  print("No users key found")

Nested when different parts can fail independently and you want different handling. Often you can flatten with multiple except:

try:
  print(f"Third user: {data['users'][2]}")
except KeyError:
  print("No users key found")
except IndexError:
  print("No third user found")

EAFP vs LBYL

Python idiom: Easier to Ask Forgiveness than Permission.

# EAFP — try and handle failure
try:
  value = my_dict[key]
except KeyError:
  value = default

# LBYL — check first
if key in my_dict:
  value = my_dict[key]
else:
  value = default

EAFP is faster when failures are rare and avoids race conditions (the value can't change between the check and the access).

For this specific case, just use my_dict.get(key, default). But the principle applies broadly: try the thing, handle if it fails.

When NOT to catch exceptions

  • Silent ignoring. except: pass swallows everything. Avoid.
  • Catching Exception broadly. Hides bugs. Catch what you can actually handle.
  • Catching to "fix" a programming bug. If your code raises TypeError, fix the code, don't catch it.

The mantra: catch what you can handle, let the rest bubble up. A traceback is a feature — it tells you what went wrong.

Common stumbles

Bare except:. Catches KeyboardInterrupt (Ctrl-C). User can't quit your program. Use except Exception: if you must catch broadly, or better: catch specific types.

Catching too late. Exception arose in deeper code; you should have caught it nearer the source.

Catching too early. Wrap a 50-line try block — when it fails, you don't know which line. Keep try blocks small.

Forgetting to log. except SomeError: pass — error happened, you'll never know. At minimum, log it.

Re-raising loses info. except SomeError: raise OtherError(...) — the original traceback is lost. Use raise OtherError(...) from e.

finally returns can swallow exceptions. If finally returns a value, it discards any pending exception. Don't return from finally.

What's next

Lesson 17: modules and imports. Splitting code into multiple files; import, from ... import, packages.

Recap

try runs risky code; except SomeError as e catches a specific exception; else runs on success; finally runs always (cleanup). raise to throw; subclass Exception for custom types. Catch specific exceptions, not bare except. Keep try blocks small. Prefer EAFP over LBYL — try, then handle. Use raise X from e to chain.

Next lesson: modules and imports.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.