Chapter 11

Exceptions and Testing

11.0 Intro 11.1 Exceptions 11.2 Unit Testing

Use ← → arrow keys or Space to navigate  |  Press F for fullscreen

Why Exceptions and Testing?

Handle failures gracefully and verify that code does what it claims

Two Kinds of Problems

Runtime failures

The program stops because Python raises an exception.

age = int("twenty")
# ValueError

Exception handling lets the program recover or report a useful message.

Semantic bugs

The program runs, but the answer is wrong.

def average(nums):
    return sum(nums) / (len(nums) - 1)
# wrong formula

Tests catch wrong behavior before users do.

Chapter 11 Toolkit

TracebacksFollow the error path to the failing line.
try / exceptRecover from expected runtime failures.
raiseSignal invalid input or state clearly.
loggingRecord what happened without cluttering output.
assertCheck assumptions while developing.
testsMake behavior repeatable and verifiable.

11.1 Exceptions

Tracebacks, try/except, else/finally, custom exceptions, and logging

Reading Tracebacks

  • Start at the bottom for the exception type and message.
  • Look upward to find the line in your code that failed.
  • Tracebacks show the call stack: who called whom.
A traceback is a map, not just an error message.
def parse_age(text):
    return int(text)

def register(raw_age):
    age = parse_age(raw_age)
    return {"age": age}

register("twenty")
# ValueError: invalid literal for int()

try / except

  • Put risky code in the try block.
  • Catch specific exception types.
  • Return, retry, or explain the failure.
Avoid bare except:. It can hide real bugs.
def parse_int(text):
    try:
        return int(text)
    except ValueError:
        print(f"{text!r} is not an integer")
        return None

print(parse_int("42"))     # 42
print(parse_int("hello"))  # None

Multiple Exceptions

Different failures deserve different responses.

ExceptionCommon cause
ValueErrorInvalid value, like int("x")
FileNotFoundErrorMissing file path
KeyErrorMissing dictionary key
ZeroDivisionErrorDivision by zero
def get_score(scores, name):
    try:
        return scores[name]
    except KeyError:
        return "missing student"
    except TypeError:
        return "scores must be a dict"

else and finally

  • else runs only if no exception occurs.
  • finally always runs, whether an exception happened or not.
  • Use finally for cleanup.
try:
    value = int("42")
except ValueError:
    print("bad input")
else:
    print("converted:", value)
finally:
    print("done")

Raising Exceptions

  • Use raise when a function cannot honor its contract.
  • Choose a specific built-in exception when possible.
  • Create custom exceptions for domain-specific failures.
class OverdraftError(Exception):
    pass

def withdraw(balance, amount):
    if amount < 0:
        raise ValueError("amount must be positive")
    if amount > balance:
        raise OverdraftError("insufficient funds")
    return balance - amount

Logging

Logging records diagnostic information without relying on scattered print() calls.

LevelUse
DEBUGDetailed developer info
INFONormal progress
WARNINGUnexpected but recoverable
ERROROperation failed
import logging

logging.basicConfig(level=logging.INFO)

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        logging.error("division by zero")
        return None

11.2 Unit Testing

pytest, unittest, doctest, fixtures, mocking, parametrization, and coverage

What is a Unit Test?

  • A small automated check for one behavior.
  • Usually tests one function, method, or class behavior.
  • Should be repeatable and independent.
Tests turn examples into executable evidence.
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

pytest

  • Test files usually start with test_.
  • Test functions usually start with test_.
  • Use plain assert statements.
Run from the terminal with python -m pytest.
# test_calc.py
from calc import add, divide

def test_add():
    assert add(2, 3) == 5

def test_divide():
    assert divide(10, 2) == 5

Testing Exceptions

Good tests check both normal behavior and expected failure behavior.

  • Use pytest.raises for expected exceptions.
  • Test the contract, not implementation details.
import pytest

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("b cannot be 0")
    return a / b

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

unittest

  • Python's built-in testing framework.
  • Organizes tests inside classes.
  • Uses assertion methods like assertEqual.
You will see unittest in many older or standard-library-oriented projects.
import unittest

class TestCalc(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)

if __name__ == "__main__":
    unittest.main()

Doctests

  • Examples embedded in docstrings.
  • Useful for simple functions and documentation.
  • Best when outputs are short and stable.
def square(x):
    """Return x squared.

    >>> square(4)
    16
    >>> square(-3)
    9
    """
    return x * x

Advanced pytest Tools

ToolUse
ParametrizeRun one test with many inputs
FixtureReusable setup data or objects
MockReplace slow or external dependencies
CoverageFind code paths tests did not run
import pytest

@pytest.mark.parametrize(
    "a,b,expected",
    [(2, 3, 5), (0, 0, 0), (-1, 1, 0)]
)
def test_add_cases(a, b, expected):
    assert add(a, b) == expected

Testing Strategy

Test these first

  • Normal cases
  • Boundary cases
  • Error cases
  • Previously fixed bugs

Keep tests useful

  • Small and focused
  • Readable names
  • Independent of test order
  • Fast enough to run often
Testing is design feedback: if code is hard to test, it is often doing too much.

Chapter 11 - Quick Reference

ConceptKey syntax / notes
Catch exceptiontry: / except ValueError:
Exception objectexcept Exception as e:
Cleanupfinally always runs
Raise exceptionraise ValueError("message")
Logginglogging.info(), logging.error()
pytest testdef test_name(): assert result == expected
Expected exceptionwith pytest.raises(Error):
unittestclass TestX(unittest.TestCase):
doctestExamples in docstrings with >>>

End of Chapter 11

Next up: OOP

tracebacks · exceptions · logging · pytest · unittest · doctest