ASAmol Shukla
Projects
Courses
Prompts
Skills
Contact
Resume
Course Outline
Syllabus Overview

Complete Python Course: From Zero to Professional

Courses/Complete Python Course: From Zero to Professional/Lesson 21: Exception Handling — Fail Gracefully
45 mins lesson duration•9 mins read

Lesson 21: Exception Handling — Fail Gracefully

try/except/else/finally, raising your own errors, and writing code that never crashes the user.

Errors Are Information

When Python hits a problem, it raises an exception. Unhandled, an exception crashes the program with an ugly traceback. Handled well, your program responds gracefully — this is the difference between professional and fragile software.

Nobody writes bug-free code. Professionals write code that anticipates failure and handles it with dignity. Every real program — websites, games, banking apps — leans on exception handling to survive bad input, missing files, and network hiccups.

What You'll Learn in This Lesson

  • Protect risky code with try/except
  • Use else and finally correctly
  • Raise your own errors with raise
  • Create custom exception classes

The try/except Shield

try: wraps risky code. If an exception occurs, execution jumps to the matching except block instead of crashing:

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
    result = None
print(result)   # None — program kept running!

Catch specific types whenever possible:

try:
    number = int(input("Enter a number: "))
except ValueError:
    print("That was not a number!")
Common exception Happens when
ValueError Wrong value (e.g., int("abc"))
TypeError Wrong type (e.g., "a" + 1)
FileNotFoundError Missing file
KeyError Missing dict key
IndexError Index out of range
ZeroDivisionError Division by zero

Mental model: except blocks are labeled boxes that catch only their matching error type. A ZeroDivisionError box won't catch a ValueError — each error falls into its own labeled box.

Catching multiple types — the tuple form:

try:
    value = int(data["price"]) / items
except (KeyError, ValueError, ZeroDivisionError):
    print("Bad data — skipping")

The Full Structure: try / except / else / finally

try:
    risky_operation()
except ValueError as e:
    print("Handled:", e)      # runs ONLY on error
else:
    print("Success!")         # runs ONLY when no error
finally:
    cleanup()                 # ALWAYS runs — even on errors
Block Runs when
try Always (the risky code)
except An exception was raised
else No exception happened
finally Always — for cleanup

Mental model: except blocks are labeled boxes that catch only their matching error type. finally is the janitor — it shows up no matter what.

Why have both else and finally? else runs only on success — so you don't accidentally handle the success path inside the try where an exception there would be caught too. finally runs unconditionally — the right place for closing files, releasing locks, and cleanup.


Raise Your Own Errors

raise lets you enforce rules and give users clear feedback:

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

try:
    set_age(-5)
except ValueError as e:
    print("Caught:", e)   # Caught: Age cannot be negative

Why raise instead of returning None? A raised error forces the caller to deal with the problem — it can't be silently ignored. Returning None often leads to a NoneType crash three lines later, far from the real bug. Raise early, raise loudly.


Custom Exceptions — Domain Errors

Subclass Exception to define errors that mean something in your domain:

class NotEnoughStock(Exception):
    pass

def order(quantity, stock=5):
    if quantity > stock:
        raise NotEnoughStock(f"Only {stock} left")
    return "Order placed"

try:
    print(order(10))
except NotEnoughStock as e:
    print("Caught:", e)   # Caught: Only 5 left

Custom exceptions are self-documenting and catchable precisely.

When to define a custom exception: when the same error can occur in many places and you want to catch it specifically, or when the error is unique to your domain (a game: GameOver; a store: OutOfStock). A hierarchy helps large codebases:

class StoreError(Exception): ...
class OutOfStock(StoreError): ...
class InvalidPayment(StoreError): ...

Now except StoreError catches every store problem while still letting you handle OutOfStock specifically.


Common Mistakes to Avoid

  • Mistake: Bare except: — Fix: it hides bugs and even catches KeyboardInterrupt. Always name the exception type.
  • Mistake: Catching Exception broadly at the top — Fix: catch specific types first, broad ones last.
  • Mistake: Swallowing errors silently (except: pass) — Fix: log or print the message — silent excepts are debugging nightmares.
  • Mistake: Raising inside else — Fix: else is for the success path; keep raises in try or normal flow.
  • Mistake: Returning None on error instead of raising — Fix: raise early so the caller must handle it.

Professional Tips & Tricks

  • Catch the most specific exception first, broad ones last.
  • Never use a bare except: — it hides bugs and catches even KeyboardInterrupt.
  • Log or print the exception message — silent except blocks are debugging nightmares.
  • Use else for success-only logic and finally for guaranteed cleanup.
  • Raise domain-specific custom exceptions so callers can catch precisely.

Key Takeaways

  • try/except catches errors and keeps the program alive.
  • else runs on success; finally always runs.
  • raise ValueError("msg") enforces rules with clear feedback.
  • Subclass Exception for domain-specific errors.
  • Catch specific types; never use bare except.
  • Raise early and loudly — never let errors vanish silently.

Next up: Iterators, generators & itertools — lazy, memory-friendly iteration.

Interactive Lesson Code Snippet
# Exception handling in action
def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Error: cannot divide by zero")
        return None
    except TypeError:
        print("Error: numbers required")
        return None
    else:
        print("Division successful")
        return result
    finally:
        print("Cleanup done")

print("10 / 2 =", divide(10, 2))
print("10 / 0 =", divide(10, 0))

# Raising with validation
def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

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

Lesson Code (Python)

# Exception handling in action
def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Error: cannot divide by zero")
        return None
    except TypeError:
        print("Error: numbers required")
        return None
    else:
        print("Division successful")
        return result
    finally:
        print("Cleanup done")

print("10 / 2 =", divide(10, 2))
print("10 / 0 =", divide(10, 0))

# Raising with validation
def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

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

Console Output

Division successful
Cleanup done
10 / 2 = 5.0
Error: cannot divide by zero
Cleanup done
10 / 0 = None
Caught: Age cannot be negative

Code Visualization Tips

  • 🧠Trace the flow: try -> (ok?) else -> finally. try -> (error!) except -> finally.
  • 🧠Picture except blocks as labeled boxes that catch only their matching error type.
  • 🧠Visualize finally as the janitor — it shows up no matter what happens.

Professional Tips & Tricks

  • ⚡Catch the most specific exception first, broad ones last.
  • ⚡Never use a bare except: — it hides bugs and catches even KeyboardInterrupt.
  • ⚡Log or print the exception message — silent except blocks are debugging nightmares.

Python Code Judge & Practice Arena

LeetCode Style

Run real Python 3.12 WebAssembly code directly in your browser against automated test suites.

Solved:0 / 20
0 / 370 XP
Challenges:
Problem 1 of 20

Problem 1: Safe Type Converter 1

Easy+10 XP
Write a function `safe_cast_1(val, target_type=int, fallback=None)` that converts val to target_type inside try/except (ValueError, TypeError), returning fallback on error.
Sample Test Cases:
Input: safe_cast_1('123', int, 0)
Expected: 123
Input: safe_cast_1('abc', int, 0)
Expected: 0
main.pyPython 3.12 (WASM)
1
2
3
4
5
6
7
8
9
10
11
12
Press Run Code to test or Submit to verify test cases

Test Your Knowledge

Instant feedback

Quick Check: Lesson 21: Exception Handling, Custom Exceptions & Robust Systems

1 / 20
What is the output of this try-except block? try: res = 10 / 0 except ZeroDivisionError: res = 'Handled' print(res)

Up next · Continue learning

Iterators, Generators & itertools

Lazy iteration with yield, memory-efficient pipelines, and the itertools toolbox.

9 mins read45 mins
Start next lesson
Previous: File Handling & Context ManagersNext: Iterators, Generators & itertools
Made withbyAmol Shukla·amolshukla.online
ASAmol Shukla

AI Developer, Trainer & Agentic AI Expert building practical learning systems and real-world AI applications.

Explore

  • Projects
  • Courses
  • Prompts
  • Skills
  • Contact
  • Experience
  • Blogs

Connect

  • Resume
  • Contact
© 2026 Amol Shukla·Created withbyamolshukla.online
Back to top