MGU Computer Science Series — Programming with Python
Part V: Files, Data, and Practical Automation
try · except · raise · finally · fail safely
Chapter 23 of 30
23
MGU Computer Science Series  ·  Programming with Python  ·  Library Textbook Edition

Chapter 23:
Exceptions and
Error Handling

How programs handle unexpected conditions gracefully — tracebacks, try and except, validation loops, file errors, raising exceptions, and designing programs that fail safely

■ Part V of VII ■

■ 10 Sections  •  Chapter 23 of 30 ■

Requires: Chapters 1–22 — functions, loops, lists, files, and CSV data

Chapter Introduction and Learning Objectives

Every program eventually meets a situation it did not expect. A user types a word where the program expects a number. A file that the program needs is missing. A calculation tries to divide by zero. A network connection drops. A list index turns out to be out of range.

In the early chapters of this book, those situations crashed programs. A traceback appeared on the screen, the program stopped, and the user saw an error message they probably did not understand. That is acceptable when you are learning. Tracebacks are useful feedback during development. But a finished program — one you give to another person, run automatically, or rely on for real work — should not crash every time something unexpected happens. It should handle the problem gracefully, explain what went wrong in plain language, and either recover or stop cleanly.

This chapter teaches you how to do that. Error handling is not an advanced topic that you add to a program at the end. It is part of program design. A program that handles errors well is a program that respects its users and its own data.

Opening Question

A program asks the user to enter a score between 0 and 100. The user types "ninety-five". What should happen? The program could crash with a ValueError traceback. It could silently store a wrong value. Or it could print a clear message and ask again. Only one of those responses is good program design. By the end of this chapter, you will know how to build programs that always take the third path — handling the unexpected without crashing, without hiding problems, and without confusing the user.

10Sections
12Objectives
13Key Terms
30Total Chapters

Learning Objectives

By the end of this chapter, you should be able to:

  1. 1Explain what an exception is and describe the circumstances that cause Python to raise one.
  2. 2Read a Python traceback and identify the exception type, the failing line, and the call chain that led to it.
  3. 3Name at least six common exception types and the situations that produce each one.
  4. 4Write a try/except statement that catches a specific exception and responds to it appropriately.
  5. 5Write a try statement with multiple except clauses that handle different exception types differently.
  6. 6Use the else and finally clauses of a try statement and explain when each one runs.
  7. 7Write a validated input loop that repeats until the user provides acceptable data.
  8. 8Handle common file errors including FileNotFoundError and PermissionError.
  9. 9Use the raise statement to signal that a function cannot complete its task.
  10. 10Use assert appropriately as a development tool for internal consistency checks.
  11. 11Apply five principles of good error-handling design to a complete program.
  12. 12Distinguish between programs that crash, programs that fail silently, and programs that fail safely — and explain why the third is best.

Sections at a Glance

Sections 23.1 – 23.3
What Exceptions Are and How to Catch Them
Tracebacks · Exception Types · The try Statement

Programs Meet Unexpected Conditions

Python programs run as a sequence of instructions. Most of the time, each instruction succeeds and execution moves to the next one. But sometimes an instruction cannot complete because of a condition the program did not anticipate.

Consider this short program:

Python
age = int(input("Enter your age: "))
print("In ten years you will be", age + 10)

This works correctly when the user types a number. But what happens if they type "twenty"?

Output (bad input)
Enter your age: twenty Traceback (most recent call last): File "age.py", line 1, in <module> age = int(input("Enter your age: ")) ValueError: invalid literal for int() with base 10: 'twenty'

The program stopped immediately. The second line never ran. The user received a message that is technically accurate but not particularly friendly.

This is an exception: a signal that something went wrong during execution that Python does not know how to handle on its own. When Python raises an exception and your code does not handle it, the program terminates and prints a traceback.

Exceptions are not the same as bugs, although a bug can cause one. An exception can be caused by:

  • Bad user input (a string where a number was expected)
  • A missing file or wrong file path
  • A value that is mathematically impossible (dividing by zero)
  • An index that is out of bounds
  • A key that does not exist in a dictionary
  • A conversion that cannot succeed

Many of these situations are not the programmer's fault and cannot be entirely prevented. A user can always type unexpected input. A file can always be missing. The right response is not to prevent these events entirely — that is often impossible — but to handle them when they occur.

The Key Idea

An exception is a signal that something unexpected happened. Handling an exception means deciding what your program should do when it receives that signal. A well-designed program handles exceptions gracefully — giving the user a clear message and continuing or exiting cleanly — rather than crashing with a raw traceback.

Exceptions and Tracebacks

When Python encounters an error it cannot handle, it raises an exception and prints a traceback: a report showing where the problem occurred and what kind of problem it was. Learning to read tracebacks is one of the most important debugging skills you can develop.

Here is an example program:

Python
def calculate_average(scores):
    total = sum(scores)
    count = len(scores)
    return total / count

results = []
average = calculate_average(results)
print("Average:", average)
Output
Traceback (most recent call last): File "average.py", line 7, in <module> average = calculate_average(results) File "average.py", line 4, in calculate_average return total / count ZeroDivisionError: division by zero
Reading a Traceback — Line by Line
Every traceback follows the same structure
  • HeaderTraceback (most recent call last): — Python shows the chain of function calls that led to the error, starting from the outermost call and ending at the line that actually failed.
  • Outer callFile "average.py", line 7, in <module> — the program called calculate_average(results) on line 7, at the top level of the script.
  • Inner callFile "average.py", line 4, in calculate_average — inside that function, line 4 is where the problem occurred.
  • Failing linereturn total / count — this is the exact line that failed.
  • ExceptionZeroDivisionError: division by zero — the exception type and a plain-English description. This is always the most useful part. Read it first.
Reading Strategy

Start at the bottom of a traceback. The exception type and message tell you what went wrong. Then work upward to find where it happened. The line at the bottom of the call chain is the immediate source of the error; the lines above it show how execution got there.

Common exception types

ExceptionWhen it occursExample
ValueErrorRight type, wrong valueint("hello")
TypeErrorOperation on wrong type"5" + 3
ZeroDivisionErrorDivision or modulo by zero10 / 0
IndexErrorList index out of rangeitems[99] when list has 5 items
KeyErrorDictionary key does not existd["missing"]
FileNotFoundErrorFile not found at given pathopen("no_such_file.txt")
NameErrorVariable or function not definedprint(undefined_var)
AttributeErrorObject lacks the attribute or method"hello".push("!")
ImportErrorModule cannot be importedimport nonexistent_module
PermissionErrorOS refuses the file operationWriting to a read-only file

The try Statement

Python handles exceptions with the try statement. The basic idea is this: you mark a block of code that might fail, and you provide a separate block of code that runs only if it does fail.

Syntax
try:
    # Code that might raise an exception
except:
    # Code that runs if an exception occurs

Here is the age-input example from Section 23.1, rewritten with a try statement:

Python
try:
    age = int(input("Enter your age: "))
    print("In ten years you will be", age + 10)
except:
    print("That doesn't look like a valid age. Please enter a number.")
Output (bad input)
Enter your age: twenty That doesn't look like a valid age. Please enter a number.

The program no longer crashes. Here is what Python does when it encounters a try statement:

  1. Python begins executing the code inside the try block.
  2. If every line in the try block succeeds, Python skips the except block entirely.
  3. If any line raises an exception, Python immediately stops executing the try block and jumps to the except block.
  4. After the except block finishes, execution continues after the entire try/except structure.
Important

When an exception occurs inside a try block, the remaining lines in that block do not run. Execution jumps directly to except. This means any code that depends on earlier lines in the try block will not have access to values that were never assigned.

Tracing a try statement

Python
try:
    x = int("abc")      # This will fail
    print("Success!")   # This will not run
except:
    print("Something went wrong.")

print("Program continues here.")
StepLineWhat happens
1x = int("abc")Raises ValueError. Python jumps immediately to except.
2print("Success!")Skipped — never runs.
3print("Something went wrong.")The except block runs.
4print("Program continues here.")Execution resumes after the try/except structure.
Output
Something went wrong. Program continues here.
Sections 23.4 – 23.6
Catching the Right Exception
Specific except clauses · Multiple handlers · else and finally

Handling Errors with except

The bare except: clause from Section 23.3 catches every exception without distinguishing between them. This is often too broad. Catching all exceptions silently can hide real bugs — if your program has a logic error that raises an unexpected exception, a bare except clause would swallow that error and make it very hard to find.

The better practice is to name the specific exception type you want to handle:

Python
try:
    age = int(input("Enter your age: "))
    print("In ten years you will be", age + 10)
except ValueError:
    print("Please enter a whole number.")

This except ValueError: clause catches only ValueError exceptions. If a different, unexpected exception occurs, it will still propagate as a traceback — which is usually what you want during development. Unexpected problems should be visible, not hidden.

Using the exception object

You can capture the exception itself using the as keyword. This gives you access to the error message Python generated:

Python
try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print("Result:", result)
except ValueError as error:
    print("Input error:", error)
except ZeroDivisionError as error:
    print("Math error:", error)
If user types "hello"
Input error: invalid literal for int() with base 10: 'hello'
If user types 0
Math error: division by zero

The variable after as (here, error) holds the exception object. Printing it produces Python's built-in description of the problem. You do not always need to print error directly — sometimes you will write your own message — but having access to it is useful for logging or giving more specific feedback.

Naming the exception

error, e, and exc are all common names for a caught exception. Any valid variable name works. Choose one that reads naturally in context. In production code, many programmers use e for brevity; in teaching examples, error is more readable.

Handling Specific Exceptions

A try statement can have multiple except clauses, each handling a different exception type. Python checks each clause in order and runs the first one that matches.

Python
def safe_divide(numerator, denominator):
    try:
        result = numerator / denominator
        return result
    except TypeError:
        print("Error: Both values must be numbers.")
        return None
    except ZeroDivisionError:
        print("Error: Cannot divide by zero.")
        return None

print(safe_divide(10, 2))      # Works: 5.0
print(safe_divide(10, 0))      # ZeroDivisionError
print(safe_divide(10, "two"))  # TypeError
Output
5.0 Error: Cannot divide by zero. None Error: Both values must be numbers. None

Catching multiple exceptions in one clause

If you want to handle several exception types the same way, list them in a tuple:

Python
try:
    value = int(input("Enter a number: "))
    result = 100 / value
except (ValueError, ZeroDivisionError):
    print("Please enter a non-zero whole number.")

Exception hierarchy

Python exceptions are organized in a hierarchy. Every exception is a subclass of Exception. Catching a parent class catches all of its children. For example, both ValueError and TypeError are subclasses of Exception. If you write except Exception:, you catch nearly all exceptions — more specific than bare except:, but still quite broad.

Design Rule

As a beginner, catch specific exception types whenever you can. It makes your code clearer, makes each handler's purpose obvious, and prevents accidentally hiding bugs that raise exception types you did not anticipate.

else and finally

The try statement has two optional additional clauses: else and finally. Together, the full structure is:

Syntax
try:
    # Code that might fail
except SomeException:
    # Runs if SomeException occurs
else:
    # Runs only if no exception occurred
finally:
    # Runs no matter what

The else clause

The else block runs only if the try block completed without raising any exception. It is useful for code that should run after a successful operation but that you do not want inside the try block itself — because any exception from that code would also be caught, possibly hiding a bug.

Python
try:
    number = int(input("Enter a number: "))
except ValueError:
    print("That is not a valid number.")
else:
    print("You entered:", number)
    print("Its square is:", number ** 2)

If the input is valid, else runs and shows the results. If a ValueError occurs, only the except block runs — else is skipped entirely.

The finally clause

The finally block runs regardless of whether an exception occurred, whether it was caught, or whether else ran. It always executes. The most common use for finally is cleanup: closing a file, releasing a resource, or resetting something to a known state.

Python
def read_config(filename):
    file_handle = None
    try:
        file_handle = open(filename, "r")
        content = file_handle.read()
        return content
    except FileNotFoundError:
        print(f"Config file '{filename}' not found.")
        return None
    finally:
        if file_handle is not None:
            file_handle.close()
            print("File closed.")
Note on with statements

In most modern Python file-handling code, the with statement (Chapter 21) handles file cleanup automatically. But finally remains important for other kinds of cleanup — database connections, network sockets, temporary files, or any resource that must be explicitly released.

All four clauses together

Example 23.6.1
Tracing a try/except/else/finally structure
  • When file existstry succeeds → except is skipped → else runs (prints word count) → finally runs (prints completion message)
  • When file missingtry raises FileNotFoundError → except runs (prints error) → else is skipped → finally runs (prints completion message)
Python
try:
    filename = input("Enter filename: ")
    with open(filename, "r") as file:
        data = file.read()
except FileNotFoundError:
    print(f"File '{filename}' was not found.")
else:
    word_count = len(data.split())
    print(f"File contains {word_count} words.")
finally:
    print("File operation complete.")
Sections 23.7 – 23.9
Practical Application: Validation, Files, and Raising
Input loops · File errors · raise · assert

Input Validation Revisited

In Chapter 8 you used while loops to keep asking for input until the user provided an acceptable value. Exception handling gives you a better tool: you can attempt the conversion inside a try block and repeat the loop only if it fails.

Pattern: validated input loop

Python
while True:
    user_input = input("Enter a whole number: ")
    try:
        number = int(user_input)
        break
    except ValueError:
        print("That is not a whole number. Please try again.")

print("You entered:", number)
InputWhat happens
"hello"int("hello") raises ValueError. except prints message. break not reached. Loop repeats.
"42"int("42") succeeds. break exits the loop.

This pattern — a while True loop that breaks only on success — is one of the most useful patterns in practical Python programs.

Validating type and range together

Python
while True:
    user_input = input("Enter a score between 0 and 100: ")
    try:
        score = int(user_input)
        if score < 0 or score > 100:
            print("Score must be between 0 and 100.")
        else:
            break
    except ValueError:
        print("Please enter a whole number.")

print("Score accepted:", score)

A reusable input function

This pattern appears often enough that it is worth turning into a function with optional minimum and maximum bounds:

Python
def get_integer(prompt, minimum=None, maximum=None):
    """Ask for an integer, repeating until valid input is given.

    Optional minimum and maximum bounds are enforced if provided.
    """
    while True:
        user_input = input(prompt)
        try:
            value = int(user_input)
            if minimum is not None and value < minimum:
                print(f"Please enter a value of at least {minimum}.")
            elif maximum is not None and value > maximum:
                print(f"Please enter a value of at most {maximum}.")
            else:
                return value
        except ValueError:
            print("Please enter a whole number.")
Using get_integer
age      = get_integer("Enter your age: ", minimum=0, maximum=150)
score    = get_integer("Enter your score (0-100): ", minimum=0, maximum=100)
quantity = get_integer("How many items? ", minimum=1)

Each call handles its own validation loop. The rest of the program receives clean, validated data and does not need to check again.

File Error Handling

File operations are among the most common sources of exceptions in practical programs. Files can be missing, in the wrong location, in the wrong format, or protected by the operating system.

Handling FileNotFoundError

Python
filename = input("Enter the filename to read: ")

try:
    with open(filename, "r") as file:
        content = file.read()
    print("File contents:")
    print(content)
except FileNotFoundError:
    print(f"Error: The file '{filename}' was not found.")
    print("Check that the filename and path are correct.")

Handling PermissionError

Python
try:
    with open("protected_file.txt", "w") as file:
        file.write("New content")
except PermissionError:
    print("Error: You do not have permission to write to that file.")

Using OSError as a fallback

Python
def read_file_safely(filename):
    """Read a file and return its contents, or None if unsuccessful."""
    try:
        with open(filename, "r") as file:
            return file.read()
    except FileNotFoundError:
        print(f"Error: '{filename}' does not exist.")
        return None
    except PermissionError:
        print(f"Error: Cannot read '{filename}' — permission denied.")
        return None
    except OSError as error:
        print(f"Error reading '{filename}': {error}")
        return None

OSError is a broader category that covers many file system problems. Catching it as a fallback after more specific exceptions is a useful defensive pattern.

Conditions versus try/except for file access

The pathlib module lets you check whether a path exists before trying to open it. Both approaches are valid:

Using pathlib conditions
Check before opening
Use Path(filename).exists() to test the path first. Clear and explicit. However, a file could be deleted between the check and the open() call in rare concurrent scenarios.
Using try/except
Attempt and catch
Attempt open() and catch FileNotFoundError if it fails. Handles the file-disappearing edge case automatically. Many programmers prefer this for robustness.

Worked example: reading a data file

Python
def load_scores(filename):
    """Load a list of scores from a text file, one per line."""
    scores = []

    try:
        with open(filename, "r") as file:
            for line_number, line in enumerate(file, start=1):
                line = line.strip()
                if line == "":
                    continue
                try:
                    score = int(line)
                    scores.append(score)
                except ValueError:
                    print(f"Warning: Line {line_number} contains '{line}', "
                          f"which is not a number. Skipping.")
    except FileNotFoundError:
        print(f"Error: Score file '{filename}' not found.")
        return None

    return scores


filename = "scores.txt"
data = load_scores(filename)

if data is not None:
    print(f"Loaded {len(data)} scores.")
    if len(data) > 0:
        print(f"Average: {sum(data) / len(data):.1f}")
Two levels of error handling
Why nested try/except blocks make sense here
  • Outer tryCatches FileNotFoundError if the file does not exist. Returns None — the caller knows the operation failed completely.
  • Inner tryCatches ValueError for individual lines that cannot be converted. Prints a warning and skips that line — the rest of the file is still processed.
  • Design choiceDifferent problems deserve different responses. A missing file is fatal to this function. A malformed line is not — we can recover and continue.

Raising Exceptions

So far, you have only caught exceptions that Python raises. But you can also raise exceptions yourself using the raise statement. This is useful when your function receives input that is technically valid Python but does not meet your program's requirements. Instead of silently continuing with bad data, you signal that something is wrong — the same way Python signals errors to you.

Raising a basic exception

Python
def calculate_square_root(number):
    if number < 0:
        raise ValueError("Cannot calculate the square root of a negative number.")
    return number ** 0.5

If you call calculate_square_root(-4), Python raises:

Output
ValueError: Cannot calculate the square root of a negative number.

The calling code can then handle this ValueError with a try/except block:

Python
try:
    result = calculate_square_root(float(input("Enter a number: ")))
    print(f"Square root: {result:.4f}")
except ValueError as error:
    print(f"Error: {error}")

Why raise exceptions?

Option A
Return a special value
Return None or -1 to signal failure. Works, but the caller must remember to check. Easy to forget and silently use the wrong value.
Option B
Print and return
Print an error and return silently. The caller receives a value without knowing it is invalid — this is often worse than crashing.
Option C — Preferred
Raise an exception
Raise an exception and let the caller decide how to handle it. The problem is explicit, visible, and cannot be accidentally ignored.

Re-raising with a better message

Python
def parse_positive_integer(text):
    """Convert text to a positive integer, or raise ValueError."""
    try:
        value = int(text)
    except ValueError:
        raise ValueError(f"'{text}' is not a whole number.")

    if value <= 0:
        raise ValueError(f"{value} is not a positive number.")

    return value

This function catches Python's built-in ValueError from int() and raises a new one with a clearer, more specific message. This is called re-raising with a better message.

Assertions as a development tool

Python
def calculate_average(scores):
    assert len(scores) > 0, "Cannot average an empty list."
    return sum(scores) / len(scores)

The assert statement checks that a condition is true, and raises AssertionError if it is not. Assertions are typically used during development to catch programming mistakes — cases where you made an incorrect assumption about your data.

When to use assert vs raise

Use raise ValueError (or another appropriate exception) for user-facing validation — when bad data arrives from user input, files, or external sources. Use assert for internal consistency checks during development — to verify assumptions about your own code. Assertions can be disabled when Python runs with the -O flag, so they should never be the only line of defense against bad data in production programs.

Section 23.10
Designing Programs That Fail Safely
Five design principles · Complete worked example

Designing Programs That Fail Safely

Exception handling is not just a technical feature. It is a design practice. A program that handles errors well is one that has been designed with realistic conditions in mind — conditions where users make mistakes, files go missing, and data turns out to be imperfect.

Principle 1
Catch at the right level
Not every function needs to handle every exception. Sometimes it is better to let an exception propagate to a higher level where more context is available to respond meaningfully.
Principle 2
Never swallow silently
A bare except: pass is almost always wrong. It hides problems and lets the program continue with missing or corrupted data. Silent failures are worse than crashes because the problem is invisible.
Principle 3
Return meaningful values
When a function cannot complete, return None (on failure) rather than an empty result that is indistinguishable from a legitimate empty result. Let callers check the return value.
Principle 4
Validate inputs early
Check data at the boundaries of your program — when it enters from the user, a file, or an external source. By the time it reaches your core logic, it should be known-good.
Principle 5
Test your error paths
When you write tests, deliberately test what happens with invalid input, missing files, and edge cases. Error-handling code that is never tested is code that might not work when you need it most.
Anti-pattern to avoid

This is dangerous code: except: pass. It swallows all exceptions without any record that something went wrong. The program continues, but it may have missing or corrupted data. Always at minimum print or log a message when catching an exception.

Bringing it together: a robust program

Here is a complete small program that applies all five principles. It reads a list of numbers from a file chosen by the user, computes statistics, and handles every foreseeable problem.

Python — robust_stats.py
def get_filename():
    """Ask the user for a filename and return it."""
    return input("Enter the data file to analyze: ").strip()


def load_numbers(filename):
    """Load numbers from a file. Return a list or None on failure."""
    numbers = []

    try:
        with open(filename, "r") as file:
            for line_number, line in enumerate(file, start=1):
                line = line.strip()
                if line == "":
                    continue
                try:
                    numbers.append(float(line))
                except ValueError:
                    print(f"  Warning: Line {line_number} ('{line}') "
                          f"is not a number. Skipping.")
    except FileNotFoundError:
        print(f"Error: File '{filename}' not found.")
        return None
    except PermissionError:
        print(f"Error: Cannot read '{filename}'. Permission denied.")
        return None

    return numbers


def compute_statistics(numbers):
    """Compute basic statistics. Raises ValueError if list is empty."""
    if len(numbers) == 0:
        raise ValueError("No numbers to analyze.")

    return {
        "count":   len(numbers),
        "total":   sum(numbers),
        "average": sum(numbers) / len(numbers),
        "minimum": min(numbers),
        "maximum": max(numbers),
    }


def display_statistics(stats):
    """Display statistics to the user."""
    print()
    print("=== Statistics ===")
    print(f"Count:   {stats['count']}")
    print(f"Total:   {stats['total']:.2f}")
    print(f"Average: {stats['average']:.2f}")
    print(f"Minimum: {stats['minimum']:.2f}")
    print(f"Maximum: {stats['maximum']:.2f}")


def main():
    filename = get_filename()
    numbers = load_numbers(filename)

    if numbers is None:
        print("Could not read the file. Exiting.")
        return

    try:
        stats = compute_statistics(numbers)
        display_statistics(stats)
    except ValueError as error:
        print(f"Error: {error}")


main()
Design Walkthrough
How the five principles appear in this program
  • Principle 1load_numbers handles file-level errors; compute_statistics raises ValueError and lets main handle it. Each function handles what it knows about.
  • Principle 2Every except clause prints a message. Nothing is swallowed silently.
  • Principle 3load_numbers returns None on file failure (distinguishable from an empty file) and a list (possibly empty) on success. main checks for None before proceeding.
  • Principle 4Numbers are validated at the point they are read from the file. By the time they reach compute_statistics, they are known to be valid floats.
  • Principle 5(Left for the exercises.) Test this program with a missing file, an empty file, a file containing non-numbers, and a file with valid data.

Chapter Summary

Key Terms

exception
An object Python raises when it encounters an error during execution that it cannot handle on its own. Exceptions carry information about the type of problem and where it occurred.
traceback
A report printed when an unhandled exception occurs, showing the chain of function calls that led to the error, the failing line, and the exception type and message.
try block
A code block marked for exception monitoring. If any line raises an exception, execution immediately jumps to the matching except block.
except clause
A block of code that runs when a specified exception type is raised inside the corresponding try block.
else clause
An optional clause in a try statement that runs only if no exception occurred in the try block. Used to separate success-path code from risky code.
finally clause
An optional clause in a try statement that runs regardless of whether an exception occurred or was caught. Used for cleanup operations.
ValueError
An exception raised when a function receives a value of the right type but an inappropriate value, such as int("hello").
FileNotFoundError
An exception raised when a file cannot be found at the given path.
ZeroDivisionError
An exception raised when a program attempts to divide a number by zero.
raise
A statement that explicitly raises an exception, signaling that a function cannot complete its task with the data it received.
assert
A statement that checks a condition and raises AssertionError if the condition is False. Used to verify internal assumptions during development, not for user-facing validation.
input validation
The process of checking that user input meets required conditions — correct type, acceptable range, expected format — before using it in a program.
fail safely
The design principle that a program should handle unexpected conditions without crashing unpredictably, silently corrupting data, or producing misleading output.
PracticeCode Reading

Study each example carefully without running it. Read it completely, then answer the questions.

Example A

Python
def get_positive_number(prompt):
    while True:
        try:
            value = float(input(prompt))
            if value <= 0:
                print("Please enter a number greater than zero.")
            else:
                return value
        except ValueError:
            print("Please enter a numeric value.")

radius = get_positive_number("Enter the radius: ")
area = 3.14159 * radius ** 2
print(f"Area: {area:.2f}")
  1. What happens if the user types "five" as the radius?
  2. What happens if the user types -3 as the radius?
  3. What happens if the user types 0?
  4. What happens if the user types 7.5?
  5. Why does this function use return value instead of break?

Example B

Python
def safe_read_line(filename, line_number):
    try:
        with open(filename, "r") as file:
            lines = file.readlines()
        if line_number < 1 or line_number > len(lines):
            return None
        return lines[line_number - 1].strip()
    except FileNotFoundError:
        return None

result = safe_read_line("notes.txt", 3)
if result is None:
    print("Could not retrieve the line.")
else:
    print("Line 3:", result)
  1. What exception does this function handle? What does it return when that exception occurs?
  2. If notes.txt exists and has 5 lines, what does safe_read_line("notes.txt", 3) return?
  3. If notes.txt exists and has 5 lines, what does safe_read_line("notes.txt", 10) return?
  4. If notes.txt does not exist, what does the function return?
  5. The function subtracts 1 from line_number when indexing lines. Why?
PracticeCode Tracing

Trace the following programs by writing the complete output for each.

Trace 1

Python
def convert(text):
    try:
        return int(text)
    except ValueError:
        try:
            return float(text)
        except ValueError:
            return None

values = ["42", "3.14", "hello", "100", ""]

for item in values:
    result = convert(item)
    print(f"'{item}' -> {result}")
  1. Write the complete output. What type does each result have?

Trace 2

Python
numbers = [10, 0, 5, 0, 2]
results = []

for n in numbers:
    try:
        result = 100 / n
        results.append(result)
    except ZeroDivisionError:
        results.append(None)

print(results)
  1. Write the complete output. What is the length of results?

Trace 3

Python
def process(data):
    try:
        value = int(data)
        print("Converted:", value)
    except ValueError:
        print("Not an integer.")
    else:
        print("Success!")
    finally:
        print("Done.")

process("42")
print("---")
process("abc")
  1. Write the complete output, including the separator line.
  2. Does "Success!" print for both calls, only one, or neither? Explain why.
PracticePredict the Output

For each code block, predict exactly what will be printed. If an unhandled exception will occur, write the exception type and explain why.

Question 1

Python
try:
    x = 10 / 2
    y = int("5")
except ZeroDivisionError:
    print("Division error")
except ValueError:
    print("Value error")
else:
    print(x + y)

Question 2

Python
items = ["apple", "banana", "cherry"]
try:
    print(items[5])
except IndexError:
    print("Index out of range")
print("Continuing")

Question 3

Python
def mystery(n):
    try:
        result = 100 // n
    except ZeroDivisionError:
        return -1
    return result

print(mystery(4))
print(mystery(0))
print(mystery(10))

Question 4

Python
data = {"name": "Jordan", "age": 19}
try:
    print(data["score"])
except KeyError:
    print("Key not found")
finally:
    print("Lookup complete")
PracticeDebugging and Error Analysis

Each program has a problem. Identify the error, explain what goes wrong, and write a corrected version.

Problem 1 — Silent swallowing

Python (buggy)
try:
    number = int(input("Enter a number: "))
    print("Double:", number * 2)
except:
    pass
  1. What is wrong with using a bare except with pass? What kinds of errors will this hide? Write a corrected version that catches ValueError specifically and prints a helpful message.

Problem 2 — Missing exception handling

Python (buggy)
while True:
    entry = input("Enter score (0-100): ")
    score = int(entry)
    if 0 <= score <= 100:
        break
    print("Out of range. Try again.")
  1. What happens if the user types "ninety"? Fix the program so it handles this gracefully without crashing.

Problem 3 — Too-broad catching

Python (buggy)
def read_number_file(filename):
    try:
        with open(filename) as file:
            numbers = [int(line) for line in file]
        return numbers
    except:
        return []
  1. This function silently swallows all errors. Rewrite it to handle FileNotFoundError and ValueError separately, with appropriate messages for each.

Problem 4 — Ambiguous return

Python (buggy)
def safe_sqrt(x):
    import math
    try:
        return math.sqrt(x)
    except ValueError:
        print("Cannot take square root of negative number.")
  1. What does this function return when x is negative? Why is that a problem for callers? Rewrite the function so its behavior is clear and callers can easily detect failure.
PracticeShort Programming Tasks
  1. Type-safe float input. Write a function get_float(prompt) that asks for a floating-point number, repeats until a valid number is entered, and returns the float.
  2. Safe list lookup. Write a function safe_get(items, index) that returns items[index] if the index is valid, or None if it is out of range. Use a condition, not try/except.
  3. Safe dictionary lookup. Write a function lookup(dictionary, key, default="N/A") that returns the value for key if it exists, or default if it does not. Handle KeyError with try/except.
  4. File line counter. Write a function count_lines(filename) that returns the number of lines in a text file, or -1 if the file cannot be opened.
  5. Validated age input. Write a program that asks for an age, validates that it is an integer between 0 and 150, and keeps asking until a valid age is entered. Print a category: "Minor" (under 18), "Adult" (18–64), or "Senior" (65 and over).
  6. Number list from string. Write a function parse_numbers(text) that splits a string on spaces, converts each word to an integer, and returns the successful conversions as a list. Words that cannot be converted should be skipped with a printed warning.
  7. Safe file appender. Write a function append_to_log(filename, message) that appends message as a new line. Return True on success, or print an error and return False if a PermissionError occurs.
Mini-ProjectRobust Calculator or Converter

Build a program called robust_calculator.py that performs basic arithmetic on values entered by the user. The program should be robust against all common input problems.

Requirements

Offer a menu: addition, subtraction, multiplication, division, and quit. For each operation, ask for two numbers and display the result. The program must handle all of the following without crashing: non-numeric input for either number; division by zero; invalid menu choices; KeyboardInterrupt and EOFError. Continue running after any error, repeating the menu until the user quits.

Required functions

  1. get_number(prompt) — asks for a number, validates it, returns a float.
  2. get_menu_choice() — asks for a menu choice, validates it.
  3. calculate(operation, a, b) — performs the chosen operation, returns the result or None on failure.
  4. display_menu() — prints the available operations.
  5. main() — runs the main program loop.

Example interaction

=== Robust Calculator === 1. Addition 2. Subtraction 3. Multiplication 4. Division 5. Quit Choose an operation (1-5): 4 Enter first number: 10 Enter second number: 0 Error: Cannot divide by zero. Choose an operation (1-5): 4 Enter first number: 10 Enter second number: 4 Result: 2.5 Choose an operation (1-5): abc Invalid choice. Please enter 1, 2, 3, 4, or 5. Choose an operation (1-5): 5 Goodbye.

Extension (optional)

Add a calculation history. Keep a list of every successful calculation during the session. Add a menu option to display it. When the user quits, save the history to calculator_history.txt. Handle all file errors gracefully.

Mixed ReviewCumulative Practice

These questions draw on skills from earlier chapters as well as Chapter 23.

  1. Safe average. Write safe_average(numbers). If the list is empty, raise ValueError. If the list contains non-numeric values, skip them with a warning. Return the average of the valid numbers, or raise ValueError if none remain.
  2. Student record loader. A CSV file contains student records: one name and one score per line, separated by a comma. Write load_student_records(filename) that returns a list of dictionaries with "name" and "score" keys. Handle: the file not existing, lines with the wrong number of fields, and scores that are not valid integers. Print warnings for malformed lines.
  3. Conditions vs. try/except. Explain the difference between Program A (using if len(items) > 0: before accessing items[0]) and Program B (using try: print(items[0]) and catching IndexError). In what situations would each be preferable?
  4. Configuration file design. A function should read a configuration file and return a dictionary of settings. If the file is missing, it should return a dictionary of defaults instead. Write this function. Should it print a message when using defaults, or stay silent? Explain your choice.
  5. Refactor this handler. The following places too much code inside the try block. Identify what can go wrong and what cannot, then rewrite the program so that only the risky operations are inside the try block:

    try:
        a = int(input("Enter a: "))
        b = int(input("Enter b: "))
        c = a / b
        d = a + b
        e = a * b
        print(a, b, c, d, e)
    except:
        print("An error occurred.")
Chapter 23 Checklist
Confirm your understanding before moving to Chapter 24
Concepts
  • I can explain what an exception is and why Python raises one.
  • I can read a traceback and identify the exception type, message, and failing line.
  • I can name at least six common exception types and what produces each one.
  • I can explain why a bare except: clause is usually too broad.
  • I can explain the difference between the else and finally clauses.
  • I can explain when to use raise versus assert.
Skills
  • I can write a try/except statement that catches a specific exception.
  • I can write a try statement with multiple except clauses for different errors.
  • I can write a validated input loop that repeats until the user provides valid data.
  • I can handle FileNotFoundError and PermissionError in file operations.
  • I can use raise ValueError to signal invalid input inside a function.
Design
  • I can identify where in a program exceptions should be caught versus propagated.
  • I can explain why silent swallowing of exceptions is dangerous.
  • I can design a function that returns a meaningful value on failure (not a misleading empty result).
  • I know that error paths should be tested deliberately, not assumed to work.
Preview — Chapter 24
Dates, Randomness, and Simulation

Chapter 24 introduces two more standard-library tools: random and datetime. You have seen random briefly in Chapter 15. Chapter 24 goes further — covering random choices, simulating repeated trials, seeding for reproducibility, and interpreting results. You will also work with dates and times in Python: creating date objects, formatting them for display, computing durations, and building programs that are aware of when they run. Everything you have learned about functions, loops, data structures, files, and exceptions will apply to the programs you build in Chapter 24.

MGU Library Connection
  • Programming with Python — Appendix D: Common Python Error Messages
  • Programming with Python — Appendix E: Debugging Checklist
  • Programming with Python — Appendix I: File and CSV Reference
  • Companion resource: Exception Handling Guide
  • Companion resource: Common Error Messages Quick Reference
  • Companion resource: Debugging Checklist (printable)
  • Previous chapter: Chapter 22 — CSV Files and Table Data
  • Next chapter: Chapter 24 — Dates, Randomness, and Simulation