# Exception Handling Basics

> Chapter 15 of the Courseiva PYTHON-PCEP curriculum — https://courseiva.com/learn/python-pcep/exception-handling-basics

**Official objective:** 5.3 — Use try-except blocks to handle common exceptions.

## Introduction

Without exception handling, one single typo in a user's input can crash your entire program, losing all the work done before it. Exception handling is your program's safety net — a way to catch errors when they happen and decide what to do next, rather than letting your program die. For the PCEP-30-02 exam, you need to know how to write these safety nets using try-except blocks, so your programs survive real-world chaos.

## The Parcel Delivery Driver Analogy

A parcel delivery driver has a route with fifty deliveries. At each house, they try to hand over the package. If nobody answers, the driver doesn't just abandon the rest of the route and drive back to the depot crying. Instead, they follow a special rule: "If no answer, leave a 'sorry we missed you' card and move on." This is the driver's 'try-except' plan. The normal plan is 'try to deliver'. The backup plan, triggered only if something goes wrong, is 'except: leave a card'. Without this plan, one unanswered door would ruin the entire day's work. 

The driver also carries a cheat sheet for different problems. If the address doesn't exist, they don't leave a card — they return the package to the depot. That's a different 'except' block for a different type of error. If the package is damaged, they take a photo and report it. Each problem has its own specific response. 

Python's exception handling works exactly this way. You write the normal steps in a 'try' block. Then you write separate rescue plans in 'except' blocks for different kinds of errors. This stops one little problem from crashing your whole program. It lets the program handle the error gracefully and keep running.

## Core explanation

Exception handling is a programming technique that lets you manage errors (called 'exceptions') that happen while your program is running, without the program crashing. Think of it like building an airbag system into your code. When your program hits a problem, instead of exploding, it deploys the airbag — a block of code you wrote specifically for that situation.

First, let's define the key terms. An 'exception' is an event that occurs during the execution of a program that disrupts the normal flow of instructions. For example, trying to divide by zero, or trying to open a file that doesn't exist. When an exception occurs, Python creates an 'exception object' that contains information about the error, like what went wrong and where.

A 'try-except block' is the structure you use to handle exceptions. You put the code that might cause an exception inside the 'try' block. Then you write one or more 'except' blocks immediately after, each one designed to catch a specific type of exception and run some rescue code.

Here's how it works step by step:

1. Python executes the code inside the 'try' block, line by line.
2. If no exception occurs, Python skips all the 'except' blocks and continues with the rest of the program.
3. If an exception occurs on any line inside the 'try' block, Python immediately stops executing the rest of the 'try' block. It then looks at the 'except' blocks below to find one that matches the type of exception that occurred.
4. If it finds a matching 'except' block, it executes that block's code. Then it continues with the rest of the program after the try-except structure.
5. If it doesn't find a matching 'except' block, the program crashes with an unhandled exception.

Let's look at a real example. Suppose you ask the user to enter a number, and they type 'hello'.

Without exception handling:
```
user_input = input("Enter a number: ")
number = int(user_input)  # If user types 'hello', this line crashes the program
print("You entered:", number)
```

With exception handling:
```
try:
    user_input = input("Enter a number: ")
    number = int(user_input)
    print("You entered:", number)
except ValueError:
    print("That wasn't a valid number. Please try again.")
```

In the second version, if the user types 'hello', the program prints "That wasn't a valid number" and keeps running. It doesn't crash.

The 'ValueError' after the word 'except' is the type of exception you're catching. Python has many built-in exception types. Here are the ones you need for the PCEP-30-02 exam:

- ValueError: Raised when a function receives an argument with the right type but an inappropriate value (like int('hello')).
- TypeError: Raised when an operation or function is applied to an object of inappropriate type (like 'hello' + 5).
- ZeroDivisionError: Raised when you try to divide by zero.
- IndexError: Raised when you try to access an index that doesn't exist in a list (like my_list[10] when the list only has 5 items).
- KeyError: Raised when you try to access a key that doesn't exist in a dictionary.
- NameError: Raised when you try to use a variable that hasn't been defined yet.
- FileNotFoundError: Raised when you try to open a file that doesn't exist.
- SyntaxError: This is special — it happens before the program runs, so you can't catch it with try-except. Python won't even start your program if it has a syntax error.

You can write multiple 'except' blocks for one 'try' block to handle different types of errors differently:

```
try:
    number = int(input("Enter a number: "))
    result = 10 / number
    print("Result:", result)
except ValueError:
    print("That's not a valid number.")
except ZeroDivisionError:
    print("You can't divide by zero.")
except Exception:
    print("Something unexpected happened.")
```

Notice the last 'except Exception:' line. 'Exception' is the base class for all built-in exceptions. So 'except Exception:' catches any exception that didn't match a previous except block. Use this carefully — it's a catch-all that can hide bugs you didn't expect.

There's also an 'else' clause you can add after all the 'except' blocks. The 'else' block runs only if no exception occurred in the 'try' block. And a 'finally' block runs no matter what — whether an exception occurred or not. The 'finally' block is great for cleanup actions, like closing a file.

The key reason exception handling exists is that it separates the normal flow of your program from the error-handling logic. Without it, you'd have to check for every possible error after every single operation, making your code hard to read and maintain. Exception handling lets you write the happy path first, then add safety nets separately.

## Real-world context

Imagine you work as a junior IT support analyst for a company that runs a customer booking system. Your colleague built a Python script that reads a CSV file of customer names and their booking dates, then sends them a reminder email. The script works perfectly in testing with clean data. But in production, the file sometimes has blank rows, or dates in the wrong format, or names with special characters. Before exception handling, the script would crash on the first bad row and stop processing all the remaining customers. Your boss would be furious because hundreds of customers never got their reminder.

Here's what you do to fix it. You wrap the part of the script that processes each row inside a 'try' block. Then you write specific 'except' blocks for the errors you know can happen.

Step by step, here's the real-world process:

1. Identify the risky operations. In this case, converting a date string to a datetime object can raise a ValueError if the format is wrong. Reading a row from a CSV can raise an IndexError if a row has fewer columns than expected. Splitting a string can raise an AttributeError if the value isn't a string.
2. Write the 'try' block that contains these risky operations.
3. Write an 'except ValueError' block that logs the bad date and skips that customer, but continues processing the rest.
4. Write an 'except IndexError' block that logs the incomplete row and moves on.
5. Write a generic 'except Exception as e' block to catch anything else unexpected, log the error, and continue. You store the error message in a variable called 'e' using the 'as' keyword so you can print or log what went wrong.
6. Add a 'finally' block to close the CSV file, ensuring the file handle is released even if an error occurs.

Here's what the code structure looks like:

```
import csv

file = open("customers.csv", "r")
try:
    reader = csv.reader(file)
    for row in reader:
        try:
            name = row[0]
            date_str = row[1]
            from datetime import datetime
            booking_date = datetime.strptime(date_str, "%Y-%m-%d")
            # send email logic here
        except (ValueError, IndexError) as e:
            print(f"Skipping bad row: {e}")
            continue
except FileNotFoundError:
    print("The customer file is missing. Contact the data team.")
finally:
    file.close()
```

Notice the structure: there's an outer try-except-finally for file-level errors, and an inner try-except for per-row errors. This nested approach is common in real code.

The practical outcome: the script now processes 99% of customers successfully, logs the problematic rows for later manual review, and doesn't crash. You've saved the company from a PR disaster. This is exactly how exception handling is used in production systems — not just for academic examples, but to build robust, fault-tolerant software that can survive real-world messy data.

In your daily work, you'll also use exception handling to debug problems. When an error occurs, you can catch the exception, print its traceback (the trail of function calls that led to the error), and understand exactly what went wrong. The traceback is Python's way of saying "Here's the path the program took before it hit the error." Reading tracebacks becomes second nature with practice.

## Exam focus

The PCEP-30-02 exam tests exception handling in objective 5.3, and it's a topic they love to trick you on. Here's exactly what you need to know.

The exam will test these specific skills:

- Writing a try-except block with the correct syntax. The word 'try' must be lowercase, followed by a colon. The indented block follows. Then 'except' (lowercase, colon) and optionally an exception type, then the indented rescue block. They will show you code with 'Try' capitalised, or missing colons, or wrong indentation — all of those are syntax errors.
- Knowing which built-in exceptions exist. You must memorise: ValueError, TypeError, ZeroDivisionError, IndexError, KeyError, NameError, and FileNotFoundError. They won't test obscure exceptions.
- Understanding the order of except blocks. More specific exceptions must come before more general ones. If you write 'except Exception:' before 'except ZeroDivisionError:', the ZeroDivisionError block will never run because Exception catches everything. The exam will give you code with blocks in the wrong order and ask if it's valid or what it prints.
- Knowing what happens when an exception isn't caught. The program terminates with a traceback. The exam will show you code that raises an exception without a matching except block and ask "What happens?" The answer is always "The program terminates with an error message."
- Recognising that 'else' runs only if no exception occurred. 'finally' runs always, regardless of exceptions. They will test this with tricky examples.
- Understanding that a 'try' block can have multiple 'except' blocks, but at least one 'except' or 'finally' is required. A 'try' without any 'except' or 'finally' is a syntax error.
- Knowing that you can catch multiple exception types in one 'except' line using a tuple: 'except (ValueError, TypeError):'. The exam will ask if this syntax is valid (it is).

Common traps the exam sets:

- They'll give you a try block that raises an exception, and then the code after the try-except structure has a variable that was only defined inside the try block. If the exception occurred before that variable was defined, using it later causes a NameError. They want you to realise that code in the except block doesn't have access to variables that were never assigned because the exception stopped execution.
- They'll write 'except:' without specifying an exception type. This is valid syntax and catches all exceptions — same as 'except Exception:'. But it's considered bad practise because it hides errors. They'll ask if it's valid (yes) and what it does (catches everything).
- They'll test the 'as' keyword: 'except ValueError as e:'. This assigns the exception object to variable 'e'. The exam might print 'e' and ask what it contains.
- They'll nest try-except blocks and ask about flow. If an exception occurs in the inner try and isn't caught there, it propagates to the outer try's except blocks. If no outer except catches it, the program crashes.
- They'll give you code with a finally block that has a return statement. A return in finally overrides any previous return in try or except. This is tricky.

Memorise this pattern: try-except-else-finally. The 'else' is optional and goes after all except blocks but before 'finally'. The 'finally' is optional and goes at the end.

Key definitions to memorise for the exam:

- Exception: An event that disrupts normal program flow.
- Try block: The block of code where you monitor for exceptions.
- Except block: The block of code that runs when a specific exception occurs.
- Else block: The block that runs if no exception occurred in the try block.
- Finally block: The block that always runs, whether an exception occurred or not.
- Raising an exception: The act of triggering an exception, either automatically by Python or manually with the 'raise' keyword. (The exam does test 'raise' in other objectives but not heavily here.)

The exam questions are typically multiple-choice with one correct answer. They show a short code snippet and ask "What is the output?" or "What is the result?" or "Is this code correct?" Practise by reading code line by line, tracking variable values, and noting where exceptions would occur.

## Step by step

1. **Identify Risky Code** — Look at your code and find lines that could fail. Common examples: converting user input to an integer, dividing by a variable, accessing a list index, opening a file. These are the lines that could raise exceptions.
2. **Write the try Block** — Put the risky code inside the 'try' block. Indent it properly. The try block can contain multiple lines. Python will execute them one by one until an exception occurs or the block finishes.
3. **Write except Blocks for Specific Exceptions** — Immediately after the try block, write one or more 'except' blocks. Each one starts with 'except' followed by the exception type (like ValueError) and a colon. Inside, write the code to handle that specific error. The code in the except block runs only if that exact exception type occurs.
4. **Add an else Block (Optional)** — If you want code that runs only when no exception occurred, add an 'else' block after all except blocks. This is useful for actions that should only happen on success, like confirming a transaction.
5. **Add a finally Block (Optional)** — If you have cleanup code that must run no matter what (like closing a file or releasing a network connection), put it in a 'finally' block. This block runs even if there's a return, break, or continue in the try or except blocks.

## Comparisons

### try-except vs if-else error checking

**try-except:**
- Separates normal logic from error handling
- Catches errors without checking every operation
- Handles unexpected errors automatically

**if-else error checking:**
- Requires explicit checks after each operation
- Makes code longer and harder to read
- Cannot handle errors you don't anticipate

### except ValueError vs except Exception

**except ValueError:**
- Catches only ValueError exceptions
- More specific and predictable
- Preferred for handling known error types

**except Exception:**
- Catches all built-in exceptions
- Can hide unexpected bugs
- Used as a catch-all fallback

### else block vs finally block

**else block:**
- Runs only if no exception occurred
- Executes after all except blocks
- Skipped entirely if an exception is caught

**finally block:**
- Runs no matter what (exception or not)
- Executes even after return statements
- Used for cleanup actions like closing files

## Diagram

_This flowchart shows the decision path when Python executes a try-except-else-finally structure, including what happens when an exception is caught or not._

```mermaid
flowchart TD
    A[Start try block] --> B[Execute line 1]
    B --> C{Exception raised?}
    C -- No --> D[Execute next line]
    D --> E{More lines?}
    E -- Yes --> B
    E -- No --> F[Run else block (if any)]
    F --> G[Run finally block]
    G --> H[Continue program]
    C -- Yes --> I{Matches except block?}
    I -- Yes --> J[Run matching except block]
    J --> G
    I -- No --> K[Unhandled exception: program crashes]
```

## Common misconceptions

- **Misconception:** If I put all my code in a try block, it will never crash. **Reality:** A try block only prevents crashes if you also have an except block that catches the exception. Without a matching except, the program still crashes. (Beginners see 'try' as a magic shield. They forget the except part because they focus only on the try word.)
- **Misconception:** Using 'except:' without specifying an exception type is always bad and should never be used. **Reality:** 'except:' is valid Python that catches all exceptions. It is acceptable in some situations, like cleanup code, but it's generally discouraged because it hides unexpected errors. (Many tutorials over-correct by saying 'never use bare except', but the exam tests that you know it's syntactically valid, even if not recommended.)
- **Misconception:** If a program hits an unhandled exception, it just skips that line and keeps running. **Reality:** An unhandled exception immediately stops the program and prints a traceback. It does not skip the line and continue. (Beginners expect programs to be forgiving. In reality, Python stops dead unless you explicitly handle the error.)
- **Misconception:** The 'finally' block only runs if an exception occurs. **Reality:** The 'finally' block runs no matter what — whether an exception occurred or not, and even if there's a return statement in the try or except blocks. (The word 'finally' sounds like 'last resort' or 'cleanup after error', so beginners assume it's error-only. The exam loves to test this.)
- **Misconception:** I can put the 'except' block before the 'try' block. **Reality:** The 'except' block must always come immediately after the 'try' block. You cannot reorder them or put other code in between. (Some beginners confuse exception handling with if-else logic, where order is flexible. The syntax is strict in Python.)

## Key takeaways

- A try-except block lets your program recover from errors gracefully instead of crashing.
- You must have at least one 'except' block or a 'finally' block after a 'try' block for the code to be valid.
- The order of except blocks matters: put more specific exception types before more general ones.
- The 'else' block runs only if no exception occurred in the 'try' block.
- The 'finally' block always runs, regardless of whether an exception was raised or caught.
- An unhandled exception causes the program to terminate immediately and display a traceback.

## FAQ

**What is the difference between 'except:' and 'except Exception:'?**

They behave the same — both catch all built-in exceptions. 'except Exception:' is clearer about what you're catching and is considered better style. 'except:' also catches exceptions that don't inherit from Exception (like SystemExit), which is rarely what you want.

**Can I have multiple except blocks for one try block?**

Yes, you can have multiple except blocks. Python checks them in order and runs the first one that matches the exception type. If none match, the exception propagates up.

**What happens if I put 'except ValueError:' before 'except Exception:'?**

That is correct order. Python checks from top to bottom, so the more specific ValueError block runs first if a ValueError occurs. If a different exception occurs, it falls through to the more general Exception block.

**Does a finally block run if there's a return statement in the try block?**

Yes, the finally block always runs. Even if the try block has a return statement, the finally block executes before the function actually returns.

**What is a traceback?**

A traceback is Python's error report that shows the sequence of function calls that led to the exception. It tells you the file, line number, and the type of exception. You see a traceback when an unhandled exception crashes your program.

**Can I catch multiple exception types in one except block?**

Yes, you can put multiple exception types in a tuple: 'except (ValueError, TypeError):'. This is valid and catches either exception type.

## Check your understanding

1. **What is the output of this code?
try:
    x = int("hello")
except ValueError:
    print("A")
except Exception:
    print("B")
else:
    print("C")
finally:
    print("D")**

   Answer: The output is A followed by D on separate lines. The int("hello") raises a ValueError, which is caught by the first except block (prints A). The else block is skipped because an exception occurred. The finally block always runs (prints D).

2. **What happens if a 'try' block has no 'except' or 'finally' block?**

   Answer: It is a syntax error. Python requires at least one 'except' block or a 'finally' block after every 'try' block.

3. **Name three built-in exceptions you must know for the PCEP-30-02 exam.**

   Answer: ValueError, TypeError, and ZeroDivisionError are three examples. Others include IndexError, KeyError, NameError, and FileNotFoundError.

4. **Is 'except (ValueError, TypeError):' valid Python syntax?**

   Answer: Yes, this is valid. It catches both ValueError and TypeError with the same except block.

5. **What is the difference between 'except:' and 'except Exception as e:'?**

   Answer: Both catch all exceptions, but 'except Exception as e:' stores the exception object in variable 'e' so you can inspect it. 'except:' does not capture the exception object.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/python-pcep/exception-handling-basics
