Grade by Grade
B. Software Development · Software Development: Problem Solving, Design, Testing and the Life Cycle

Detecting and Handling Exceptions

B3.3About 70 minutes
Learning goals
  • I can explain what an exception is and recognize common ones: ZeroDivisionError, ValueError and IndexError.
  • I can detect a problem before it happens with an if statement (a guard).
  • I can intercept and handle an exception with try and except so a program does not crash.
  • I can design algorithms that handle division by zero, square roots of negative numbers and invalid input.

Words to know

exception
An error that happens while a program is running (a run-time error), such as dividing by zero. If nothing handles it, the program stops.
traceback
The error report Python prints when an unhandled exception stops a program. Its last line names the exception.
guard
An if statement that checks for a problem before the risky step, such as if len(values) == 0:.
try / except
Python statements that run risky code (try) and, if a named exception happens, run handling code (except) instead of crashing.
handle
To respond sensibly to an exception: explain the problem, ask again, use a safe default or report it.
raise
To deliberately signal an exception, for example when a function receives a value it cannot work with.
discriminant
The value b² − 4ac in the quadratic formula. If it is negative, the equation has no real roots.

When good programs meet bad data

Your average calculator works perfectly... until someone runs it on an empty list. Your temperature converter works... until someone types minus five in words. In both cases Python stops with a scary traceback. A user sees a crash; a business might lose an order.

These run-time errors are called exceptions. The Ontario curriculum asks you to design algorithms that detect, intercept and handle them, with division by zero and roots of negative numbers as the classic examples. You met run-time errors in Programming Basics; now you will design programs that survive them.

CodeExceptionLast line of the traceback
10 / 0ZeroDivisionErrorZeroDivisionError: division by zero
int("abc")ValueErrorValueError: invalid literal for int() with base 10: 'abc'
math.sqrt(-1)ValueErrorValueError: math domain error (Python 3.14 and later print a more detailed message, but it is still a ValueError)
[80, 90][2]IndexErrorIndexError: list index out of range
Common exceptions. Reading the last line of a traceback tells you which one happened.

Two ways to deal with exceptions

1. Detect it first (a guard). Check with if before doing the risky step. This is best when the problem is easy to test, like an empty list or a negative number.

2. Intercept it (try / except). Put the risky step inside try:. If the named exception happens, Python jumps to the matching except block and runs your handling code instead of crashing. This is best when checking in advance is hard, such as whether a string is a valid number ("-3.5", "1e3" and " 7 " are all valid!).

Either way, handling means doing something sensible: explain the problem in plain words, ask again, use a safe default, or report the problem to the code that called you.

Worked example

Example 1: A guard against division by zero

Make average(values) safe for an empty list.

  1. Find the risky step: sum(values) / len(values) divides by len(values), which is 0 for an empty list.
  2. Decide what the function should do instead. There is no average of nothing, so return the special value None (meaning "no value").
  3. Add the guard before the division: if len(values) == 0: return None.
  4. Test the normal case and the extreme case.

The safe function prints 80.0 for [70, 80, 90] and None for [].

def average(values):
"""Return the mean of values, or None if there are none."""
if len(values) == 0: # detect the problem before it happens
return None
return sum(values) / len(values)
print(average([70, 80, 90]))
print(average([]))

Output: 80.0 / None (one line each)

try:risky code runsdid anexception happen?except ValueError:handle it: explain,ask again or use a defaultprogram continues(no crash)yesnoWithout try/except, the exceptionstops the program with a traceback.
How try and except work. If the risky code raises the named exception, the except block handles it; either way, the program carries on.
Worked example

Example 2: Intercepting bad input with try / except

Write get_float(prompt) that keeps asking until the user types a number, then use it in a temperature converter.

  1. The risky step is float(text): it raises ValueError for text like "cold".
  2. Put it inside try: and return the number at once if it works.
  3. In except ValueError: print a friendly message (what went wrong, and an example of what to type).
  4. Wrap everything in while True: so the question repeats until a return happens.
  5. Catch only ValueError: catching every possible error with a bare except: would also hide real bugs.

With the inputs cold and then -5, it explains the problem once and then prints In Fahrenheit: 23.0.

def get_float(prompt):
"""Keep asking until the user types a number; return it."""
while True:
text = input(prompt)
try:
return float(text)
except ValueError:
print(f"'{text}' is not a number. Try something like 12 or -3.5.")
temp = get_float("Temperature in C: ")
print("In Fahrenheit:", temp * 9 / 5 + 32)

Sample run: the user types cold (handled: 'cold' is not a number. Try something like 12 or -3.5.), then -5. Output: In Fahrenheit: 23.0.

total = 120.0
people = int(input("How many people share the bill? "))
try:
share = total / people
print(f"Each person pays ${share:.2f}")
except ZeroDivisionError:
print("At least one person must pay. Nobody was charged.")

Intercepting division by zero. Sample run: the user types 0. Output: At least one person must pay. Nobody was charged. With 4 it would print Each person pays $30.00.

Roots of negative numbers

In the real numbers, a negative number has no square root, so math.sqrt(-4) raises a ValueError (the message is math domain error, or a more detailed one in Python 3.14 and later). This matters in the quadratic formula, x = (−b ± √(b² − 4ac)) ÷ 2a. The discriminant d = b² − 4ac tells you what to expect: two real roots if d > 0, one if d = 0, none if d < 0. A well-designed algorithm checks d first instead of crashing. It also must not divide by 2a when a = 0.

b² − 4ac > 0two real rootsb² − 4ac = 0one real rootb² − 4ac < 0no real rootsx
The discriminant decides how many times the parabola crosses the x-axis. When it is negative, there is no real root, so the algorithm must not take its square root.
Worked example

Example 3: A quadratic solver that never crashes

Design real_roots(a, b, c) that returns a list of the real roots of ax² + bx + c = 0.

  1. a = 0 is not a quadratic and would divide by zero. The function cannot give a sensible answer, so it raises a ValueError with a clear message; the caller decides what to do.
  2. Compute d = b² − 4ac. Guard: if d < 0, return an empty list (no real roots).
  3. If d = 0, return one root, −b ÷ 2a.
  4. Otherwise compute both roots with math.sqrt(d), which is now safe, and return them smallest first.
  5. Test all four paths: x² − 5x + 6 (roots 2 and 3), x² − 4x + 4 (root 2), x² + 2x + 5 (none) and a = 0.

The output is [2.0, 3.0], [2.0], [] and Error: a must not be 0 in a quadratic.

import math
def real_roots(a, b, c):
"""Return a list of the real roots of ax^2 + bx + c = 0."""
if a == 0:
raise ValueError("a must not be 0 in a quadratic")
d = b * b - 4 * a * c
if d < 0:
return [] # no real roots
if d == 0:
return [-b / (2 * a)]
r1 = (-b - math.sqrt(d)) / (2 * a)
r2 = (-b + math.sqrt(d)) / (2 * a)
return [min(r1, r2), max(r1, r2)]
print(real_roots(1, -5, 6))
print(real_roots(1, -4, 4))
print(real_roots(1, 2, 5))
try:
print(real_roots(0, 2, 5))
except ValueError as error:
print("Error:", error)

Output: [2.0, 3.0] / [2.0] / [] / Error: a must not be 0 in a quadratic (one line each)

Good exception-handling habits

  • Catch specific exceptions (except ValueError:), not everything.
  • Keep the try block small: only the line that can fail, plus what depends on it.
  • Never handle an exception by hiding it silently (except: pass). Tell the user, use a default on purpose, or raise it again.
  • Messages say what went wrong and how to fix it (see the interfaces lesson).
  • Test every handler: an exception handler that is never tested is a bug waiting to happen.
Just for fun

Python calls dividing by zero a ZeroDivisionError. Math teachers have been raising that exception by hand, with a red pen, for centuries.

Hands-on activity

Crash-proof challenge

You need: a computer with Python, and a family member who enjoys breaking things.

  1. Write a "bill splitter" program: it asks for the bill total and the number of people, and prints each person's share.
  2. Hand it to your family member. Their job is to make it crash: letters, blank answers, 0 people, negative numbers, huge numbers, decimals for people. Write down every input that crashed it and the exception's name (last line of the traceback).
  3. Fix each one with a guard or a try/except, so the program explains the problem and asks again.
  4. Repeat until they cannot crash it. Keep your list: it is the start of a test plan (next lessons).

What to look for: ValueError for letters or blanks, ZeroDivisionError for 0 people, and a sensible rule for negatives (they do not crash but should be rejected). The final program handles every case with a clear message.

Your turn

1

What is an exception?

2

Name the exception raised by each: 5 / 0, int("five"), [1, 2, 3][3].

3

What does the program print? try: / x = 1 / 0 / print("A") / except ZeroDivisionError: / print("B"), and after the try/except, print("C").

4

Write a guard that stops math.sqrt(x) from being called when x is negative, returning None instead.

5

Use real_roots to find the roots of 2x² + 3x − 2 = 0.

6

What does real_roots(1, 0, 9) return, and why?

7

Which of these strings does float() accept: "7", "7.5", "seven", "", "1e3"?

More practice: warm-up

1

What does the last line of a traceback tell you?

2

Which keyword starts the block of risky code?

3

Is math.sqrt(-9) a syntax error or a run-time error?

4

What is a guard?

5

If the discriminant b² − 4ac is negative, how many real roots are there?

More practice: on your own

1

Write a function to_int(text, default) that returns int(text), or default if the text is not a whole number. What does it return for "42", "4.2" and "abc" (with defaults 0, 0 and −1)?

2

Explain when a guard is a better choice than try/except, with an example of each.

3

Why is except: with no exception name (or except: pass) considered bad practice?

4

Write code that prints the third mark in marks = [80, 90] or, if there is none, prints "No third mark".

5

Find the roots of x² + 6x + 9 = 0 with real_roots, and explain which branch runs.

Challenge

1

In the bill splitter, people = int(input(...)) comes before the try block. Which bad inputs still crash the program? Redesign it so no input can crash it.

2

Why does real_roots raise an exception for a = 0 instead of printing a message?

3

Design an algorithm (pseudocode) for a calculator that reads two numbers and an operator (+, −, ×, ÷, √ of the first number) and never crashes. List every exception it must handle.

Check your understanding

Quiz

1.

Which exception does int("12a") raise?

2.

What is printed? try: print(10 / 2) except ZeroDivisionError: print("oops")

3.

A guard is:

4.

For x² + 2x + 5 = 0, the discriminant is:

5.

Which is the best practice?

6.

What does average([]) return in the safe version from the lesson?

For parents and teachers

Big idea: B3.3 asks students to design algorithms that detect, intercept and handle exceptions, such as division by zero and roots of negative numbers. Two tools are taught: guards (if checks) and try/except.

How to help: Try to break your teen's programs! Type letters where numbers are expected, enter 0, leave answers blank. Every crash you find is a lesson, and a robust program is something to be proud of.

Common difficulties:

  • Putting the risky line (often int(input())) outside the try block.
  • Catching every exception with a bare except:, which hides real bugs.
  • Forgetting the extreme cases that cause exceptions: empty lists, zero, negative numbers.
  • Confusing exceptions (run-time errors) with syntax errors, which stop the program before it even starts and cannot be caught this way.