Exception Handling (try, except, raise, finally, custom exceptions) - Python Tutorial #16
Video: Exception Handling (try, except, raise, finally, custom exceptions) - Python Tutorial #16 by Taught by Celeste AI - AI Coding Coach
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: passswallows everything. Avoid. - Catching
Exceptionbroadly. 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.