Programming in Python · Lesson 10

Exceptions

Logic errors vs. runtime errors, default and specific exception types, try-except blocks, catching specific exceptions, and raising your own exceptions in Python.

  • python
  • exceptions
  • try-except
  • error-handling

Lesson purpose

Programs meet situations they cannot handle on their own: a file that is missing, a user who types letters instead of a number, a request that breaks the function’s assumptions. This lesson introduces exceptions, the mechanism Python uses to signal that something unusual happened and to hand the problem to code that can deal with it. Students learn how exceptions differ from logic errors, what default and specific exception types are, how try/except blocks change program flow, how to catch specific exceptions and read their messages with as, how to raise their own exceptions, and how to decide when raising an exception is the right design choice.

For students who know C# and JavaScript: Python’s try/except is the same idea as try/catch in both languages, and raise is Python’s throw. Python exception types such as ValueError play the role of C#’s ArgumentException family.

Learning objectives

By the end of the lesson, students should be able to:

  • Explain why exceptions are raised in situations where the computer cannot continue, and define an exception as a triggered event.
  • Describe the difference between a logic error and a runtime error.
  • Define exception objects as packages of information about what happened.
  • Contrast default exceptions with specific exception types, and explain that different types represent different general errors.
  • Explain that each language defines its own exception types, and name common Python exception types.
  • Explain what it means for code to raise (throw) an exception.
  • Use a try-except block to handle exceptions, and trace the program flow through it.
  • Limit a try block to the code that depends on the operation that might fail.
  • Catch specific exceptions, use multiple except statements, and access an exception’s message with as.
  • Raise exceptions from your own functions, and decide when a function should handle a problem itself and when it should raise an exception.

1. Defining exceptions

Errors in code

Debugging means finding and fixing errors. Two kinds matter here:

Kind What happens How you find it
Logic error The code runs and produces output, but part of it is wrong (for example, checking for numbers greater than zero when you meant less than zero) Carefully reading and tracing the code
Runtime error A flaw that makes the program fail during execution The program crashes, usually with a traceback

The book also mentions semantic errors, which it describes as errors caught by the compiler.

Exceptions

The book’s running example is Iceabella, a video-game character who makes ice cream as long as you bring her enough ice and cream. What should she do if you ask for ice cream before giving her any cream? Try anyway? Shut the machine down? Or tell you what is wrong so you can fix it?

Ideally, a program that cannot do what it was asked should fail gracefully and tell the user why. Code also has preconditions, assumptions that must be true for it to work. If a precondition is not met, the code should fail gracefully instead of crashing.

An exception is an unexpected event that occurs as the program runs and prevents the program from completing execution. The unusual situation is said to trigger the exception and interrupt the normal program flow.

An error is a design flaw that debugging should fix. An exception is different: it lets one part of a program that cannot solve a problem on its own ask another part for help.

main program ──calls──► code that may cause exceptions
      ▲                         │
      │  no exception: continue │  exception: ask for help
      │  as normal              ▼
more of the main   ◄──  part of the main program
program                 that handles exceptions

When the main program runs code that might raise an exception, flow continues in one of two ways:

  • If no exception occurs, flow returns to the main program (with a result, if one was requested).
  • If an exception occurs, flow is diverted to the part of the program that handles exceptions. That code resolves the problem and the program keeps running.

A robust program checks for exceptions where it could not otherwise continue and responds appropriately, instead of letting an unwanted situation halt the program.

Exceptions as packages of information

An exception is also an object: a package of information about what went wrong. It can be as simple as a numeric code or string description, or a bundle that includes the program’s state and the exception type.

Default vs. specific exceptions

  • Default exceptions are general, nonspecific exceptions a language provides for all problems. They are easy to catch but give little help fixing the problem. “Something bad happened” helps less than “I have no cream for the ice cream.” A message such as “Unknown error occurred” means a default exception was used.
  • Specific exception types represent specific kinds of errors, so programmers can find solutions faster. “Invalid parameter type” tells you to look at the parameters.

Common Python exception types:

Type Situation Example that raises it
IndexError An index is outside an appropriate range numbers = [] then numbers[2]
NameError A name (variable or function) is not defined nonexistent_function()
NotImplementedError A function or method was not implemented, but other code tries to run it Raised on purpose by a placeholder: raise NotImplementedError
RuntimeError An error that does not fit any other type raise RuntimeError("Not enough cream")
TypeError A function or operation is given an argument of the wrong type "cat" + 5
ValueError A function is given an argument with the right type but an incorrect value int("abc"), math.sqrt(-5)
ZeroDivisionError An attempt to divide by zero x = 4 / 0

Correction: the book lists calling nonexistent_function() as an example of NotImplementedError. Running it actually raises NameError, because the name was never defined. NotImplementedError is raised deliberately by code, usually in a function stub that has not been written yet.

Each language publishes its own list of exception types, and programmers are responsible for knowing (or looking up) the ones in their language. Most languages, Python included, also let you define your own exception types, such as a NoIceCream exception, or attach a custom message to an existing type such as RuntimeError.

2. Handling exceptions

Raising and handling

When code causes an exception, programmers say the code raises (or throws) the exception. Handling the exception matters more than investigating why it was raised: if Iceabella has no cream, the fix is to give her cream, whatever happened to the old supply.

Example: an image editor uses the Python Image Library (PIL) to open image files. The library cannot know whether your program should show an error message or a default image when a file is missing, so instead of choosing for you, it raises an exception and lets your code decide.

To work with exceptions, a program tries to do something and catches any exceptions raised along the way.

The try-except block

A try-except block (called try-catch in some languages) has two parts:

  • The try block attempts to run as if conditions are normal.
  • If an exception is raised anywhere in the try block, execution stops at that point and jumps to the start of the except block.

If no exception is raised, the except block is skipped. Either way, the program continues with the line after the whole try-except block.

try:
    function1()
    print("Function 1 called.")
    function2()
    print("Function 2 called.")
except:
    print("An exception was raised.")
print("Continuing program.")

If function1() raises an exception, neither print("Function 1 called.") nor function2() runs. Unlike an if-else statement, a try block stops mid-block when it detects an exception.

The except block can fix the problem or stop the program. Fixing can mean using default values for missing information, displaying an error message, or logging the error to a file.

Put dependent code inside the try

This version has a problem:

from PIL import Image
cat_image = None
try:
    cat_image = Image.open("cat.png")
except:
    print("Could not display a cat picture.")
cat_image.show()

cat_image.show() only works if Image.open() succeeded. If the file is missing, the message prints and then cat_image.show() crashes because cat_image is still None. Include in the try block all the code that relies on the operation that might fail:

from PIL import Image
cat_image = None
try:
    cat_image = Image.open("cat.png")
    cat_image.show()
except:
    print("Could not display a cat picture.")

Catching specific exceptions

A bare except: catches every exception, so its message must be generic. Naming the exception type lets you give a more helpful message:

from PIL import Image
cat_image = None
try:
    cat_image = Image.open("cat.png")
    cat_image.show()
except FileNotFoundError:
    print("Could not find the cat picture file.")

You can include one except for each exception you expect. UnidentifiedImageError comes from the PIL module, so it is imported along with Image:

from PIL import Image, UnidentifiedImageError
cat_image = None
try:
    cat_image = Image.open("cat.png")
    cat_image.show()
except FileNotFoundError:
    print("Could not find the cat picture file.")
except UnidentifiedImageError:
    print("Could not identify the picture format.")

How much code belongs in the try?

The Iceabella menu program calls make_ice_cream(), add_cream(amount), and add_ice(amount). Assume the only possible exceptions are adding a negative amount of cream or ice, or making ice cream with no cream or no ice.

Too much in the try (not ideal): wrapping the whole loop means the first exception jumps out of the loop and the program ends.

choice = -1
amount = 0
print("Welcome to the Fantasy Ice Cream Shop! What do you want to do?")
try:
    while choice != 0:
        print("  1: Make ice cream")
        print("  2: Add cream")
        print("  3: Add ice")
        print("  0: Quit")
        choice = int(input("> "))
        if choice == 1:
            make_ice_cream()
        elif choice == 2:
            amount = int(input("How much? > "))
            add_cream(amount)
        elif choice == 3:
            amount = int(input("How much? > "))
            add_ice(amount)
        print()
except:
    print("Something went wrong.")

Better: wrap only the function-call section, inside the loop, so the user sees the message and gets the menu again.

while choice != 0:
    # ...print the menu...
    choice = int(input("> "))
    try:
        if choice == 1:
            make_ice_cream()
        elif choice == 2:
            amount = int(input("How much? > "))
            add_cream(amount)
        elif choice == 3:
            amount = int(input("How much? > "))
            add_ice(amount)
        print()
    except:
        print("Something went wrong.")

Professional design keeps only the lines that rely on exception-raising code inside the try block.

General syntax and ordering

try:
    # Code that might raise
    # exceptions somewhere...
except RuntimeError:
    print("A runtime error happened.")
except AttributeError:
    print("An attribute error happened.")
except:
    print("Any other exception ends up here.")
  • A generic except: (one that names no exception) must be the last option, like else in an if-elif-else chain.
  • Use only one except per exception type.
  • Python checks the except clauses top to bottom and runs the first one that matches.

Clarifying note: putting the generic except: before specific ones is not just poor style; Python refuses to run it and reports SyntaxError: default 'except:' must be last.

Beyond the book: Python’s try statement also supports else: (runs only if no exception occurred) and finally: (always runs, exception or not). These match C#’s finally and are handy for cleanup.

3. Using exceptions

Raising your own exceptions

A raise statement acts like a bailout: the function stops immediately and control returns to where it was called, carrying the exception information. Iceabella without exceptions:

cream_amount = 0
ice_amount = 0

def make_ice_cream():
    global cream_amount
    global ice_amount
    cream_amount -= 1
    ice_amount -= 1
    print("Ice cream was made!")

def add_cream(amount):
    global cream_amount
    cream_amount += amount
    print("Thanks for the", amount, "cups of cream!")

def add_ice(amount):
    global ice_amount
    ice_amount += amount
    print("Thanks for the", amount, "cups of ice!")

Adding checks that raise exceptions:

def make_ice_cream():
    global cream_amount
    global ice_amount
    if cream_amount <= 0:
        raise RuntimeError("Not enough cream to make ice cream.")
    elif ice_amount <= 0:
        raise RuntimeError("Not enough ice to make ice cream.")
    cream_amount -= 1
    ice_amount -= 1
    print("Ice cream was made!")

def add_cream(amount):
    if amount < 0:
        raise ValueError("Cannot add negative amounts of cream.")
    global cream_amount
    cream_amount += amount
    print("Thanks for the", amount, "cups of cream!")

def add_ice(amount):
    if amount < 0:
        raise ValueError("Cannot add negative amounts of ice.")
    global ice_amount
    ice_amount += amount
    print("Thanks for the", amount, "cups of ice!")

Notes:

  • The text in parentheses becomes the exception’s message.
  • No else block is needed after the checks: once an exception is raised, the rest of the function does not run.
  • A raised exception may travel up through a chain of function calls; it is caught by the nearest enclosing try-except block.

Reading the message with as

Writing except Exception as e: stores the exception object in the variable e. Converting it with str(e) gives the message passed to raise:

while choice != 0:
    # ...print the menu and read choice...
    try:
        if choice == 1:
            make_ice_cream()
        elif choice == 2:
            amount = int(input("How much? > "))
            add_cream(amount)
        elif choice == 3:
            amount = int(input("How much? > "))
            add_ice(amount)
    except Exception as e:
        error_message = str(e)
        print(error_message)
    print()

Sample run: choosing 1 with no supplies prints Not enough cream to make ice cream. and the menu appears again; adding 10 cream and 10 ice and then choosing 1 prints Ice cream was made!.

C# comparison: except Exception as e: is catch (Exception e), and str(e) is like e.Message. raise ValueError("...") is like throw new ArgumentException("...").

When to bail out

Exceptions let a function “bail out” of a situation it cannot complete, like an emergency brake that stops and finds someone who knows how to fix the problem. The design question is when to handle a problem yourself and when to raise an exception.

  • If the function would have to make up a solution to continue, let someone else handle it. If Iceabella went off to find cream herself, she would defeat the purpose of the game’s interactions.
  • Think about what the function is being asked to do (“make ice cream”). If the input makes that request impossible, raise an exception. Making ice cream assumes the ingredients are available; when those assumptions break, raise instead of completing.
  • If your functions raise exceptions often, rethink the design and its assumptions.
  • Raising exceptions only when your code truly cannot continue keeps each function focused on its own goal (making ice cream) and leaves other goals (getting cream) to other parts of the program.
  • Raising exceptions for every small inconvenience makes a library tedious to use and pushes extra work onto other programmers.

Ask: Can I fix this on my own? How much trouble does it cause the caller? For example, asking Iceabella to clean a shop that is already clean is not worth an exception (there is nothing to do), but a bucket filled with something that is not ice is.

Classroom application

  1. Logic or runtime? Give students short buggy snippets and have them classify each bug as a logic error or a runtime error before running it.
  2. Name that exception. Show one-line snippets ([][2], "cat" + 5, int("abc"), 4 / 0, an undefined name) and have students predict the exception type, then check in the shell.
  3. Safe number input. Write a loop that keeps asking for an integer until int(input()) succeeds, catching ValueError.
  4. Trace the flow. Given a try block with three statements, have students mark which lines run when the first, second, or third statement raises an exception.
  5. Right-size the try. Rework the Iceabella menu so only the function calls are inside the try, and confirm the menu keeps running after an error.
  6. Raise your own. Write withdraw(balance, amount) that raises ValueError for a negative amount and RuntimeError for insufficient funds; call it inside a try-except that prints str(e).

Common misconceptions

  • “Exceptions and bugs are the same thing.” A bug is a design flaw to fix; an exception is a signal that a situation the code cannot handle has occurred.
  • “The rest of the try block still runs after an exception.” Execution jumps to except at the failing line.
  • “The code after the try block is safe even if the try failed.” Code that depends on the risky operation belongs inside the try.
  • “A bare except: is the best choice because it catches everything.” It also hides unexpected bugs and gives only generic messages; catch specific types when you can.
  • “The generic except: can go anywhere.” It must be last, or Python reports a SyntaxError.
  • “Calling an undefined function raises NotImplementedError.” It raises NameError.
  • “raise just prints a message.” It stops the function and sends an exception object to the nearest handler; if none exists, the program crashes with a traceback.
  • “Every problem should raise an exception.” Raise only when the function truly cannot continue.

Lesson summary

  • An exception is an unexpected event that occurs as the program runs and prevents it from completing execution; the unusual situation triggers the exception and interrupts normal program flow.
  • A robust, well-written program checks for exceptions where it could not otherwise continue and responds appropriately.
  • Default exceptions are general and do not give context for solving the problem; specific exception types identify the kind of error and help programmers find solutions faster. Exception types differ by language.
  • When code causes an exception, it “raises” the exception. Programs try to do something and catch any exceptions raised in that code.
  • A try-except (try-catch) block runs the try code as if conditions are normal; if an exception is raised, flow moves to the except block.
  • A try block stops executing when it detects an exception; the except block can fix the problem (default values, an error message, logging) or stop the program.
  • Specific except clauses give more helpful messages; a generic except must come last, and except Exception as e gives access to the message.
  • You can raise your own exceptions with raise, and a raised exception is caught by the nearest try-except block.
  • Limit the code inside a try-except block to the code that relies on the part that might fail, and raise exceptions only when a function cannot reasonably continue.