Chapter 10 - Exceptions

1. Handling exceptions

1.1 Why We Need Error-Checking Code

Programs often deal with user input, files, networks, math operations, etc.
These can fail or behave unexpectedly.

Examples:

  • User types "One-hundred fifty" instead of "150"

  • Dividing by zero

  • File not found

  • Wrong data type

These errors are called exceptions.


1.2 What Is an Exception?

An exception is an unexpected error that occurs during program execution.

Example:

int("One-hundred fifty")

This produces a ValueError, which normally crashes the program.


1.3 Goal of Exception Handling

Instead of crashing, the program should:

✔ Handle the error
✔ Show a helpful message
✔ Keep running normally

This is where try/except comes in.


1.4 The try / except Structure

Basic pattern:

try:
    # code that might cause an exception
except:
    # code to handle the exception

How it works:

  1. Python executes everything inside the try block.

  2. If no error occurs → skip the except block; continue normally.

  3. If an exception happens:

    • Python jumps to the except block

    • Skips remaining statements inside the try block

    • Then continues running the rest of the program


1.5 Example Scenario (BMI Program)

User enters:

"One-hundred fifty"

This causes:

int(user_input)  # ValueError

Without a try/except, the entire program stops.

With try/except:

try:
    weight = int(input("Enter weight: "))
except ValueError:
    print("Error: Please enter a number.")

Now the program handles the mistake gracefully.


1.6 Why Not Just Manually Check Everything?

You could write custom code like:

for c in user_input:
    if not c.isdigit():
        print("Invalid input!")

BUT:

  • It complicates the code

  • It misses many real-world error cases

  • Try/except is cleaner, shorter, and safer


1.7 Key Points About try Execution Flow

If no exception:

  • All code in try executes

  • except is skipped

If an exception occurs:

  • Python jumps immediately to except

  • Remaining statements in try are not executed

Example:

try:
    a = 5
    b = int("hello")  # exception occurs here
    c = 10            # never executed
except:
    print("Bad input!")


Quick Cheatsheet

Use try/except when:

  • Converting user input

  • Opening a file

  • Doing math that might fail

  • Working with networks or APIs

Syntax:

try:
    risky_code()
except SomeError:
    handle_issue()

Behavior:

  • Prevents program crashes

  • Keeps code clean

  • Lets you recover from bad input or unexpected events


2. Multiple handlers

2.1 Why Handle Multiple Exception Types?

Different lines of code may cause different types of errors.
Example:

  • int("abc") → ValueError

  • dividing by zero → ZeroDivisionError

  • using an unknown variable → NameError

  • adding incompatible types → TypeError

A single try block may need to deal with more than one possible error.


2.2 Multiple except Blocks

Python allows multiple exception handlers, one for each type of error:

try:
    # risky code
except ValueError:
    print("Bad number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")

How it works:

  • Python executes only the first matching except block.

  • Other handlers are ignored after that.


2.3 Catchall except: — Use With Caution

A catchall handler:

except:
    print("Something went wrong")

will catch any error, even unexpected ones.

Good practice:

  • Avoid using bare except: unless absolutely necessary.

  • It can hide bugs, making debugging harder.


2.4 What Happens If an Exception Isn't Handled?

If Python encounters an exception with no matching except block:

  • It becomes an unhandled exception

  • The program prints a traceback

  • The program halts

Example:

Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero


2.5 Handling Multiple Exception Types in One Block

Sometimes multiple exception types should produce the same response.
Use a tuple:

except (ValueError, TypeError):
    print("Invalid input!")

This block handles either ValueError or TypeError.


2.6 BMI Example (Expanded)

If the user enters:

  • "abc" → ValueError

  • "0" for height → ZeroDivisionError

A program can handle both:

try:
    bmi = weight / (height * height)
except ValueError:
    print("You must enter numbers.")
except ZeroDivisionError:
    print("Height cannot be zero.")


Quick Cheatsheet

Multiple handlers:

try:
    ...
except ValueError:
    ...
except TypeError:
    ...

Catch multiple types:

except (ValueError, TypeError):

Avoid catchall:

except:
    # not recommended

Unhandled exception:

Program stops and prints an error.


3. Raising exceptions

3.1 The Problem With Naive Error Checking

A beginner might add if-else checks everywhere:

if weight < 0:
    print("Invalid weight.")
else:
    if height < 0:
        print("Invalid height.")
    else:
        bmi = weight / (height * height)
        print(bmi)

Why this is bad:

  • Normal program flow becomes cluttered
    You can no longer clearly see:
    get weight → get height → compute BMI

  • Duplicate checks
    You might check for negative values multiple times.

  • Easy to introduce inconsistencies
    Ex: using < in one place but <= in another.

This leads to messy, fragile code.


3.2 Cleaner Design: Use Exceptions + raise

Instead of mixing error checks into the normal code, you:

  1. Put all normal code in a try block.

  2. If something is wrong, use raise to trigger an exception.

  3. Handle errors neatly in except blocks.


3.3 How raise Works

A raise statement creates and throws an exception immediately, exiting the try block.

Example:

raise ValueError("Invalid weight.")

This creates a ValueError object with a helpful message, and jumps to the matching except block.


3.4 Improved BMI Program Using Exceptions

try:
    weight = int(input("Enter weight in pounds: "))
    if weight <= 0:
        raise ValueError("Invalid weight.")

    height = int(input("Enter height in inches: "))
    if height <= 0:
        raise ValueError("Invalid height.")

    bmi = weight * 703 / (height * height)
    print(f"BMI: {bmi}")

except ValueError as excpt:
    print(excpt)

Why this is better:

✔ Normal flow is clear:
get weight → get height → compute BMI

✔ Error handling is separated
All validation happens inside simple checks, but the reaction to errors is grouped in one place.

✔ Messages are stored in the exception
The as excpt part binds the error to a variable so you can print the message:

print(excpt)


3.5 Why ValueError?

Python exceptions communicate what kind of mistake occurred.

  • ValueError = the input value is wrong

  • TypeError = value type is wrong

  • NameError = something doesn’t exist

  • etc.

Using the correct exception type makes your code:

  • more readable

  • easier to debug

  • easier to integrate with other systems


3.6 Key Ideas to Remember

✔ Raising an exception:

  • Immediately stops the try block

  • Sends control to the except block

  • Allows you to attach a custom message

✔ Using exceptions keeps your code clean:

  • No messy nested if-else chains

  • Valid logic stays visible and readable

  • Error handling is grouped and consistent

✔ Use as excpt when you want access to the error message.


3.7 What printing the exception shows

If you write:

raise ValueError("Invalid height.")

and the except block says:

except ValueError as excpt:
    print(excpt)

the output will be:

Invalid height.

Because the exception object stores the message you passed in.


4. Exceptions with function

4.1 Key Idea: Exceptions Travel Up the Call Stack

If code inside a function raises an exception and that function does not handle it, Python:

  1. Immediately exits the function

  2. Goes back to the caller to look for a matching except block

  3. If not found, it goes further up the call chain

  4. If still not found → unhandled exception → program stops

This automatic climbing up the call hierarchy is what makes exceptions powerful.


4.2 Why This Is Useful

It keeps normal logic clean.

Example program (conceptually):

def get_weight():
    w = int(input("Enter weight: "))
    if w <= 0:
        raise ValueError("Invalid weight.")
    return w

def get_height():
    h = int(input("Enter height: "))
    if h <= 0:
        raise ValueError("Invalid height.")
    return h

try:
    weight = get_weight()     # if ValueError → jumps to except
    height = get_height()     # if ValueError → jumps to except
    print(weight * 703 / (height * height))
except ValueError as excpt:
    print(excpt)

What happens if the user enters “-40” for weight?

  • get_weight() checks, finds an error, runs:
    raise ValueError("Invalid weight.")

  • The exception is not handled inside get_weight(), so Python:

    • Immediately leaves get_weight()

    • Returns to the caller (try block in main script)

    • Finds except ValueError

    • Executes the handler


4.3 Why This Is Cleaner Than Returning Special Values

Without exceptions, you'd need something like:

w = get_weight()
if w == -1:
    print("Invalid weight.")
else:
    h = get_height()
    if h == -1:
        print("Invalid height.")
    else:
        print("BMI:", ...)

Problems:

  • Lots of nested if-else statements

  • Harder to read

  • Easy to introduce mistakes

  • Violates clean separation between normal flow and error handling

Exceptions solve this:

  • Normal flow is simple:
    get weight → get height → compute BMI

  • Error-handling code is centralized in one place

  • No need to return fake values like -1 or None


4.4 Exception Propagation = Better Structure

How it flows:

get_weight()
    ↓  raises ValueError
(no handler here)
    ↓
main script try block
    ↓  finds matching except ValueError
executes except block

This is called exception propagation.


4.5 Benefits of Using Exceptions in Functions

✔ Cleaner function design

Functions focus on what they do, not how to handle bad input.

✔ Fewer bugs

No need for sentinel return values like -1 or None.

✔ Improves readability

Error-handling is separated from main logic.

✔ Reusable functions

get_weight() does not need to know how errors will be handled — any caller can handle it.


Core Takeaways

  • A raised exception immediately exits the function.

  • If not handled inside the function, Python looks for a handler in the caller.

  • This continues up the chain until a matching handler is found.

  • This makes code clean, readable, and modular.

  • Without exceptions, you'd need messy branching logic.


5. Using finally to clean up

5.1 Purpose of finally

The finally block contains code that must run no matter what happens — whether:

  • the try block succeeds,

  • a handled exception occurs,

  • an unhandled exception occurs,

  • or the try block exits early (via return, break, or continue).

➤ Main use case:

Performing cleanup actions, such as:

  • closing files

  • releasing resources

  • cleaning temporary data


5.2 Execution Rules for finally

✔ If NO exception occurs:

  • try block runs successfully

  • finally block runs next

  • program continues normally

✔ If a HANDLED exception occurs:

  • try block runs until error

  • correct except block runs

  • finally block runs afterward

✔ If an UNHANDLED exception occurs:

  • try runs until error

  • no matching except

  • finally still runs

  • then the exception is re-raised

✔ If the try block is exited early:

finally still runs even if try contains:

  • return

  • break

  • continue


5.3 Structure With finally

try:
    # risky code
except SomeError:
    # handle specific error
finally:
    # always runs (cleanup code)

finally must always appear last in a try/except structure.


5.4 Example Use Case: File Handling

A program tries to read integers from a file.
Even if errors occur (file missing, invalid data), finally ensures the file is closed.

try:
    f = open("data.txt")
    for line in f:
        num = int(line)  # may raise ValueError
except Exception as e:
    print("Error:", e)
finally:
    f.close()  # ALWAYS executes


Key Takeaways

  • finally always executes — success, failure, or interruption.

  • Ideal for cleanup actions.

  • Works with or without specific except blocks.

  • Prevents resource leaks (like open files).

  • Must be the last clause in the try structure.


6. Custom exception types

6.1 Raising Built-In Exceptions

Python provides many built-in exception types that you can raise directly.

Example:

if my_num < 0:
    raise ValueError("my_num < 0")

Use built-in exception types whenever they clearly describe the error:

  • ValueError

  • TypeError

  • ZeroDivisionError

  • KeyError

  • etc.


6.2 Why Create Custom Exceptions?

Custom exception types are useful when:

  • You want to represent program-specific errors.

  • Built-in exceptions don't clearly describe the situation.

  • You want exception handlers to catch only your custom, meaningful errors.

  • You’re writing larger modules/libraries.


6.3 How to Define a Custom Exception

A custom exception is just a class that inherits from Python’s built-in Exception class.

Example:

class LessThanZeroError(Exception):
    def __init__(self, value):
        self.value = value

Or a minimal version:

class LessThanZeroError(Exception):
    pass

➡ A class must contain at least one statement (pass works if nothing else is needed).


6.4 Raising a Custom Exception

Just like built-ins, but using your new type:

if my_num < 0:
    raise LessThanZeroError("Number must not be negative")


6.5 Naming Convention

Good practice:
👉 Custom exception names should end with "Error"
Examples:

  • LessThanZeroError

  • FileFormatError

  • InvalidMoveError

This matches Python's naming style and makes the purpose obvious.


6.6 Handling a Custom Exception

Handled exactly like built-in exceptions:

try:
    do_something()
except LessThanZeroError as e:
    print("Error:", e)


6.7 How Much Code Should a Custom Exception Class Have?

Usually very little.
Custom exceptions are typically simple and only store:

  • a message

  • any needed data

Why?
Because exceptions aren't used like normal classes; they only need to carry information to the handler.


Key Takeaways

  • Use built-in exceptions when appropriate.

  • Create custom exceptions for application-specific error types.

  • Custom exceptions inherit from Exception.

  • Keep custom exception classes simple.

  • Name them ending with "Error" for clarity.