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.