Free Study Material
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.
CBSE draws a clear line between two different kinds of errors, and it's worth keeping them separate in your head:
You'll never see an exception without your program having actually started running first.
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.
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.
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!")
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.
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.
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: Theexcept ZeroDivisionErrorblock only catches aZeroDivisionError— exactly that type, nothing else. A different exception happening in the sametryblock would still crash the program, because noexcepthere names it.
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 bareexcept Exception:that catches almost anything — but only as a last resort, placed after every specificexceptyou actually expect. Catching everything with nothing more specific hides exactly which problem occurred.
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
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 theelseblock is not caught by theexceptblocks above it — they only watch thetryblock. Keepelsefor code that depends on the try block having succeeded, not for more risky operations of its own.
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.
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?"
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)")
A working recap of exception handling, tying together the specific error types Python raises and the try/except/finally structure that catches them.
int("hello").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.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 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.
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.
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? |
|---|---|
| ZeroDivisionError | Dividing a number by 0. (e.g., 5/0) |
| ValueError | Right type, but wrong value. (e.g., int("Hello")) |
| TypeError | Wrong type entirely. (e.g., adding 5 + "A") |
| IndexError | Trying to access a list item that doesn't exist. |
| KeyError | Looking for a Dictionary key that isn't there. |
| NameError | Using a variable you haven't created yet. |
| ImportError | Trying to import a module that doesn't exist. |
| IOError | File 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()
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? |
|---|---|
| ZeroDivisionError | Dividing a number by 0. (e.g., 5/0) |
| ValueError | Right type, but wrong value. (e.g., int("Hello")) |
| TypeError | Wrong type entirely. (e.g., adding 5 + "A") |
| IndexError | Trying to access a list item that doesn't exist. |
| KeyError | Looking for a Dictionary key that isn't there. |
| NameError | Using a variable you haven't created yet. |
| ImportError | Trying to import a module that doesn't exist. |
| IOError | File 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.")
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.