Exceptions & Testing
5.0 Intro · 5.1 Exceptions · 5.2 Unit Testing
← → or Space to navigate · F for fullscreen
Handle failures gracefully and verify that code does what it claims
The program stops because Python raises an exception.
age = int("twenty") # ValueError
Exception handling lets the program recover or report a useful message.
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.
Tracebacks, try/except, else/finally, custom exceptions, and logging
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
Avoid bare except:. It can hide real bugs.
except:
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
Different failures deserve different responses.
ValueError
int("x")
FileNotFoundError
KeyError
ZeroDivisionError
def get_score(scores, name): try: return scores[name] except KeyError: return "missing student" except TypeError: return "scores must be a dict"
else
finally
try: value = int("42") except ValueError: print("bad input") else: print("converted:", value) finally: print("done")
raise
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 records diagnostic information without scattered print() calls.
print()
DEBUG
INFO
WARNING
ERROR
import logging logging.basicConfig(level=logging.INFO) def divide(a, b): try: return a / b except ZeroDivisionError: logging.error("division by zero") return None
pytest, unittest, doctest, fixtures, mocking, parametrization, and coverage
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_
assert
Run from the terminal with python -m pytest.
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
Good tests check both normal behavior and expected failure behavior.
pytest.raises
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
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()
def square(x): """Return x squared. >>> square(4) 16 >>> square(-3) 9 """ return x * x
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 is design feedback: if code is hard to test, it is often doing too much.
try:
except ValueError:
except Exception as e:
raise ValueError("message")
logging.info()
logging.error()
def test_name(): assert result == expected
with pytest.raises(Error):
class TestX(unittest.TestCase):
>>>
Next: Chapter 6 — Lists
tracebacks · exceptions · logging · pytest · unittest · doctest