Free Study Material

Exception Handling

What Is an Exception?

Sometimes a program is written correctly and still fails while it's running. Not because you made a typing mistake, but because something happened at runtime that the program has no way to handle — asking Python to divide by zero, or trying to use a value that isn't the type you expected. Python calls this kind of runtime failure an exception.

An exception isn't a bug in how you typed your code. It's Python telling you, mid-run, that it hit a situation it cannot continue from — and unless you do something about it, your program stops right there.

Syntax Errors vs. Exceptions

CBSE draws a clear line between two different kinds of errors, and it's worth keeping them separate in your head:

  • A syntax error breaks one of Python's grammar rules — a missing colon, a mismatched bracket. Python catches this before your program ever starts running, while it's still reading your code.
  • An exception (a runtime error) only shows up after your program has already started running, the moment a specific line hits a condition it can't handle. The code was grammatically valid — Python happily started executing it — it just ran into trouble partway through.

You'll never see an exception without your program having actually started running first.

Watch One Happen

Here's a small, perfectly valid piece of Python. Press Run and watch what happens.

total_marks = 450
number_of_subjects = 0
average = total_marks / number_of_subjects
print("Average marks:", average)

Python stops the instant it reaches the division — total_marks / number_of_subjects — because dividing by zero is mathematically undefined. The print line right after it never runs at all. Instead of your output, you get a traceback: Python's report of exactly where, and why, the program stopped.

Note: A traceback isn't Python complaining for no reason — it's genuinely useful information. It names the exact line that failed and the exact type of exception that was raised (here, ZeroDivisionError), which is the first thing you should read when something goes wrong.

Why This Matters

Notice that Python didn't skip the bad line and carry on — it stopped the entire program right there, even though the print statement immediately after was perfectly fine on its own. That's the real problem an unhandled exception creates: one risky line, deep inside a much bigger program, can bring the whole thing down.

That's exactly the gap the next topic closes — Python gives you a way to catch an exception when it happens and decide what your program should do instead of just stopping.

Tip: When you're staring at a traceback, read the last line first — it names the actual exception and what went wrong. The lines above it just show how the program got there.

Summary

An exception is a runtime error — one that only appears once your program is already running, unlike a syntax error, which Python catches before execution even begins. Left unhandled, a single exception stops your entire program immediately, wherever it happens. Python reports it as a traceback, and reading the last line first tells you exactly what went wrong.

print("Step 1: Program starts")

numerator = 100
denominator = 0

print("Step 2: Attempting math...")

result = numerator / denominator

print("Step 3: Program finishes")
print("Step 1: Program starts")

numerator = 100
denominator = 0

print("Step 2: Attempting math safely...")

try:
    # Python tries this dangerous code
    result = numerator / denominator
    print("Result is:", result)
    
except ZeroDivisionError:
    # If it crashes, it jumps here instead of dying
    print("ERROR CAUGHT: You cannot divide by zero!")

# Because we caught the error, the program survives to print this!
print("Step 3: Program finishes successfully!")

The try-except Shield

Last topic ended with a program that crashed — one unhandled ZeroDivisionError brought the whole thing down, even though the line right after it was perfectly fine. Python gives you a way to stop that from happening: wrap the risky line in a try-except shield.

total_marks = 450
number_of_subjects = 0

try:
    average = total_marks / number_of_subjects
    print("Average marks:", average)
except ZeroDivisionError:
    print("Cannot calculate an average — number of subjects is zero.")

Everything that might fail goes inside the try block. If it fails with the exact exception named after except, Python jumps straight into that block instead of crashing — the program keeps running.

How Python Decides Which except Runs

Python runs the try block one line at a time, exactly as normal, until either it finishes with no problems, or a line inside it raises an exception. The moment that happens, Python stops executing the rest of the try block immediately and looks for an except naming that same exception type.

If nothing goes wrong at all, the except block is never even glanced at — Python runs the whole try block start to finish and moves straight past it.

Watch It Catch

Run the code from the top of this topic. Instead of the traceback you saw last time, you get a plain, readable message — the program keeps going afterward instead of stopping cold.

Note: The except ZeroDivisionError block only catches a ZeroDivisionError — exactly that type, nothing else. A different exception happening in the same try block would still crash the program, because no except here names it.

Catching Different Problems Differently

A try block can go wrong in more than one way, and you can give each way its own except:

total_marks = 450

try:
    number_of_subjects = int(input("Enter number of subjects: "))
    average = total_marks / number_of_subjects
    print("Average marks:", average)
except ValueError:
    print("Please enter a whole number.")
except ZeroDivisionError:
    print("Cannot calculate an average — number of subjects is zero.")

Typing text where a number is expected raises ValueError; typing 0 raises ZeroDivisionError — two different mistakes, caught by two different except blocks, each with its own message. Python checks them top to bottom and runs the first one that matches, so put the most specific exceptions first.

Tip: You can also write a bare except Exception: that catches almost anything — but only as a last resort, placed after every specific except you actually expect. Catching everything with nothing more specific hides exactly which problem occurred.

Summary

A try block holds code that might fail; each except after it names one specific exception and what to do if that exact one happens. If nothing goes wrong, every except is skipped entirely; if something does, Python runs the first matching one, top to bottom, and the program keeps going instead of crashing. But try/except isn't the whole picture — the next topic covers two more clauses that let you control exactly what runs after it, depending on whether it succeeded.

# TODO: Wrap the risky line below in a try-except block
result = 50 / 0

print("I am still alive! The program did not crash.")
try:
    num = int(input("Enter a number to divide 100 by: "))
    print(100 / num)
    
# TODO: Add except ValueError here

# TODO: Add except ZeroDivisionError here

Code That Runs Only on Success: else

The last topic's try block held everything that might fail. Python also gives you a place to put code that should run only when nothing failed — an else clause, right after the last except.

total_marks = 450
number_of_subjects = 5

try:
    average = total_marks / number_of_subjects
except ZeroDivisionError:
    print("Cannot calculate an average — number of subjects is zero.")
else:
    print("Average marks:", average)

If the try block finishes with no exception, Python runs the else block next. If an exception happened and an except caught it, else is skipped entirely — it genuinely means "only if nothing went wrong," not just "the code after this."

Warning: An exception raised inside the else block is not caught by the except blocks above it — they only watch the try block. Keep else for code that depends on the try block having succeeded, not for more risky operations of its own.

Code That Always Runs: finally

A finally clause goes one step further than else — it runs every single time, whether an exception happened or not, and whether it was caught or not.

total_marks = 450
number_of_subjects = 0

try:
    average = total_marks / number_of_subjects
except ZeroDivisionError:
    print("Cannot calculate an average — number of subjects is zero.")
finally:
    print("Calculation attempt finished.")

Even though number_of_subjects is zero here and the except block runs, "Calculation attempt finished." still prints — finally doesn't care whether the try block succeeded, failed, or was caught. Change number_of_subjects to a real number and run it again: the except block never runs, but finally still prints, every time.

Tip: finally is the natural place for cleanup that has to happen either way — closing a file you opened, for instance, whether reading it worked or not. You'll see exactly this pattern again once file handling starts.

Putting else and finally in Order

Python only accepts one syntax order: try, then one or more except blocks, then an optional else, then an optional finally — in exactly that sequence.

total_marks = 450
number_of_subjects = 5

try:
    average = total_marks / number_of_subjects
except ZeroDivisionError:
    print("Cannot calculate an average — number of subjects is zero.")
else:
    print("Average marks:", average)
finally:
    print("Calculation attempt finished.")

With number_of_subjects set to 5, the try block succeeds, so else runs and prints the average — and finally still runs right after it, printing its own message too. Both else and finally can fire in the same run; they answer two different questions, "did it succeed?" and "did it happen at all?"

Summary

An else clause runs only when the try block finishes with no exception at all — skip it entirely the moment anything is caught. A finally clause runs every time, no matter what happened in try, except, or else, which makes it the right place for cleanup code. Written together, the order is always try → except → else → finally.

try:
    print("1. Opening calculator...")
    result = 100 / 0
except ZeroDivisionError:
    print("2. Error: Cannot divide by zero!")
else:
    print("2. Calculation successful.")
finally:
    # TODO: This line runs in both scenarios.
    print("3. Cleanup Complete (Closing App)")

Introduction

A working recap of exception handling, tying together the specific error types Python raises and the try/except/finally structure that catches them.

Concept

  • ValueError — raised when a value is the right type but the wrong content, like int("hello").
  • ZeroDivisionError — raised when dividing by zero.
  • IndexError — raised when accessing a list position that doesn't exist.
  • KeyError — raised when accessing a dictionary key that doesn't exist.
  • except Exception — a generic catch-all, always placed last, after every specific except — Python checks except blocks top to bottom, and a broad Exception block placed first would swallow errors that more specific blocks were meant to handle individually.
  • finally — runs every single time, whether an exception occurred or not.

Worked Example

def safe_divider():
    try:
        user_input = input("Enter a number to divide 100 by: ")
        number = int(user_input)
        result = 100 / number
        print("Result:", result)
    except ValueError:
        print("Error: You must enter a NUMBER.")
    except ZeroDivisionError:
        print("Error: You cannot divide by ZERO.")
    except Exception:
        print("An unexpected error occurred.")
    finally:
        print("--- Calculation Attempt Finished ---")

safe_divider()

Entering "0" triggers ZeroDivisionError; entering "hello" triggers ValueError — two different problems, two different except blocks, each with its own specific message. finally prints regardless of which one fired, or even if neither did.

Tip: Multiple except blocks are checked top to bottom, and only the FIRST matching one runs — order specific exceptions before the generic Exception catch-all, never after.

IndexError and KeyError in Practice

IndexError and KeyError follow the exact same catch pattern — only the trigger is different: a list position or a dictionary key that doesn't exist.

scores = [88, 92, 79]
student = {"name": "Aisha", "marks": 91}

try:
    print(scores[5])
except IndexError:
    print("Error: That position doesn't exist in the list.")

try:
    print(student["attendance"])
except KeyError:
    print("Error: That key doesn't exist in the dictionary.")

scores only has positions 0, 1 and 2, so scores[5] raises IndexError. student has no "attendance" key, so student["attendance"] raises KeyError. Each is caught by the except that names it, exactly like ValueError and ZeroDivisionError were caught above.

Summary

Different problems raise different, specifically-named exceptions (ValueError, ZeroDivisionError, IndexError, KeyError) — catch the specific ones you expect first, use a generic except Exception last as a safety net, and finally for cleanup code that must run regardless of what happened.

The Field Guide to Errors

Python has a specific name for every mistake. If you know the name, you know the fix. Here is your "Cheat Sheet" for Class 12.

Exception Name When does it happen?
ZeroDivisionErrorDividing a number by 0. (e.g., 5/0)
ValueErrorRight type, but wrong value. (e.g., int("Hello"))
TypeErrorWrong type entirely. (e.g., adding 5 + "A")
IndexErrorTrying to access a list item that doesn't exist.
KeyErrorLooking for a Dictionary key that isn't there.
NameErrorUsing a variable you haven't created yet.
ImportErrorTrying to import a module that doesn't exist.
IOErrorFile missing or disk full during read/write.
# -----------------------------------------------------
# CHALLENGE: Catching Math & Type Errors
# -----------------------------------------------------
# We have a robust calculator below.
# It tries to divide '100' by the user's input.
#
# YOUR TASK:
# 1. Run it and enter '0'. See the ZeroDivisionError message.
# 2. Run it and enter 'hello'. See the ValueError message.
# 3. Notice how the 'finally' block runs every single time.

def safe_divider():
    try:
        user_input = input("Enter a number to divide 100 by: ")
        number = int(user_input)  # Might raise ValueError
        result = 100 / number     # Might raise ZeroDivisionError
        print("Result: " + str(result))
        
    except ValueError:
        print("Error: You must enter a NUMBER (like 5, 10).")
        
    except ZeroDivisionError:
        print("Error: You cannot divide by ZERO.")
        
    except Exception:
        print("An unexpected error occurred.")
        
    finally:
        print("--- Calculation Attempt Finished ---")

# Run the function
safe_divider()

The Field Guide to Errors

Python has a specific name for every mistake. If you know the name, you know the fix. Here is your "Cheat Sheet" for Class 12.

Exception Name When does it happen?
ZeroDivisionErrorDividing a number by 0. (e.g., 5/0)
ValueErrorRight type, but wrong value. (e.g., int("Hello"))
TypeErrorWrong type entirely. (e.g., adding 5 + "A")
IndexErrorTrying to access a list item that doesn't exist.
KeyErrorLooking for a Dictionary key that isn't there.
NameErrorUsing a variable you haven't created yet.
ImportErrorTrying to import a module that doesn't exist.
IOErrorFile missing or disk full during read/write.
# -----------------------------------------------------
# CHALLENGE: The Safe DataFetcher
# -----------------------------------------------------
# We have a list of students and a dictionary of scores.
#
# YOUR TASK:
# 1. Wrap the print statements in a try-except block.
# 2. Catch 'IndexError' for the list.
# 3. Catch 'KeyError' for the dictionary.

students = ["Amit", "Priya", "Rohan"]
scores = {"Amit": 90, "Priya": 85}

try:
    # 1. Try accessing a student who doesn't exist (Index 5)
    print(students[5]) 
    
    # 2. Try accessing a score for "Rohan" (He is in list, but not in scores dict)
    print(scores["Rohan"])

except IndexError:
    print("Error: Student index out of range!")

except KeyError:
    print("Error: Score not found for this student!")

print("Data fetch complete.")

The Safety Net (Generic Exception)

Sometimes, an error happens that you didn't predict (like a MemoryError or SystemError).

You can use a generic except Exception: at the very end. This catches anything that slipped past the specific handlers.

Warning: Always put specific errors (like ValueError) first, and the generic Exception last. Otherwise, the generic one will catch everything and hide useful details.

Want the full course?

Unlock all chapters, video lectures, and SQL practice exercises.

Get Full Access