Python · Testing with pytest · Beginner

Assertions in Depth

5 tasks

Master pytest's assertion rewriting for clear, informative failure messages.

How pytest Rewrites Assertions

#
Python's `assert` statement on its own is minimal: if the expression is falsy, it raises `AssertionError` with no useful information: ```python # plain Python assert assert result == expected # → AssertionError (just the exception, nothing else) ``` pytest solves this with **assertion rewriting**. When pytest imports your test file, it transforms the bytecode so that every `assert` captures both sides of the comparison before evaluating it. On failure, you see exactly what differed: ``` # comparing lists: AssertionError: assert [1, 2, 3] == [1, 2, 4] At index 2 diff: 3 != 4 # comparing dicts: AssertionError: assert {'name': 'Alice', 'age': 31} == {'name': 'Alice', 'age': 30} Differing items: {'age': 31} != {'age': 30} # comparing strings: AssertionError: assert 'hello earth' == 'hello world' At position 6 diff: 'e' != 'w' ``` This makes failures self-diagnosing. You see *what* differed, not just *that* something differed. **`==` vs `is`** `==` tests *equality* (same value). `is` tests *identity* (the exact same object in memory). Use `is` for booleans and `None`: ```python assert result is True # result is the boolean True assert result == True # also works, but 1 == True is also True (less strict) assert result is not None # clear and idiomatic ``` For numbers, strings, and collections, always use `==`. The `is` check on strings and integers can give unexpected results due to Python's object interning. **Negation:** ```python assert x != y # not equal assert x not in lst # not in collection assert not condition # general negation ``` **Custom failure messages:** Add a message after the assertion — pytest shows it alongside the assertion itself: ```python assert score >= 50, f"Expected score ≥ 50, got {score}" # → AssertionError: Expected score ≥ 50, got 42 # assert 42 >= 50 ``` **Floating-point comparisons:** `0.1 + 0.2` in Python is not exactly `0.3` due to binary floating-point representation. Use `pytest.approx`: ```python import pytest assert 0.1 + 0.2 == pytest.approx(0.3) # default: 1e-6 relative tolerance assert result == pytest.approx(1.5, rel=1e-3) # within 0.1% assert result == pytest.approx(0.0, abs=1e-9) # absolute tolerance (better near zero) ``` Use `rel` (relative) when the magnitude is large; use `abs` (absolute) when the expected value is at or near zero (dividing by zero in the relative check would be a problem).

Assertion Patterns in Practice

#
**Equality, inequality, and ordering:** ```python def test_equality(): assert 2 + 2 == 4 assert 'hello' != 'world' assert 10 > 5 assert 3 <= 3 def test_negation(): assert not False assert not [] # empty list is falsy assert not '' # empty string is falsy assert not 0 # zero is falsy ``` **Membership and type checks:** ```python def test_membership(): items = ['apple', 'banana', 'cherry'] assert 'banana' in items assert 'grape' not in items def test_types(): items = ['apple', 'banana', 'cherry'] assert isinstance(items, list) assert isinstance(42, int) assert isinstance(3.14, float) # isinstance also accepts tuples of types: assert isinstance(42, (int, float)) # matches either ``` **Dict and string checks:** ```python def test_dict_assertions(): user = {'name': 'Alice', 'age': 30, 'active': True} assert 'name' in user # key exists assert user['name'] == 'Alice' # value is correct assert 'email' not in user # key absent assert len(user) == 3 # exact size def test_string_assertions(): msg = 'Hello, World!' assert msg.startswith('Hello') assert msg.endswith('!') assert 'World' in msg assert len(msg) == 13 assert msg.upper() == 'HELLO, WORLD!' ``` **Floats with pytest.approx:** ```python import math, pytest def test_sqrt(): assert math.sqrt(2) == pytest.approx(1.41421, rel=1e-4) def test_float_arithmetic(): # 0.1 + 0.2 = 0.30000000000000004 in Python assert 0.1 + 0.2 == pytest.approx(0.3) def test_near_zero(): result = 1e-10 - 1e-10 assert result == pytest.approx(0.0, abs=1e-15) ``` **Custom failure messages for context:** ```python # process_batch is a placeholder — illustrates the pattern, not a runnable snippet def test_with_context(): results = process_batch([1, 2, 3, 4, 5]) assert len(results) == 5, f"Expected 5 results, got {len(results)}: {results}" ```

Assertion Cheatsheet

#
```python # Equality / inequality assert x == y assert x != y # Ordering assert x > y assert x >= y assert x < y assert x <= y # Membership assert item in collection assert item not in collection # Type assert isinstance(obj, SomeClass) # also matches subclasses assert type(obj) is int # exact type only # Truthiness assert value # truthy (non-zero, non-empty, not None) assert not value # falsy # Floats assert x == pytest.approx(y) # default rel tolerance 1e-6 assert x == pytest.approx(y, rel=1e-3) # 0.1% tolerance assert x == pytest.approx(y, abs=0.01) # absolute tolerance ±0.01 # Strings assert s.startswith('prefix') assert s.endswith('suffix') assert 'substring' in s # Custom message shown on failure assert condition, f"Descriptive message: got {value}" ```
01

Assert a Temperature Converter

#

Write tests for `to_celsius(f)` that converts Fahrenheit to Celsius using `(f - 32) * 5 / 9`. Assert the result for at least three values: 32°F (0°C), 212°F (100°C), and 98.6°F (37°C). Use `pytest.approx` for float comparisons.

import pytest


def to_celsius(f):
    return (f - 32) * 5 / 9


def test_to_celsius():
    pass
Solution
import pytest


def to_celsius(f):
    return (f - 32) * 5 / 9


def test_freezing_point():
    assert to_celsius(32) == pytest.approx(0.0)

def test_boiling_point():
    assert to_celsius(212) == pytest.approx(100.0)

def test_body_temperature():
    assert to_celsius(98.6) == pytest.approx(37.0, rel=1e-3)
02

Use pytest.approx for Circle Area

#

Implement `circle_area(r)` that returns `π * r²` using `math.pi`. Write tests that verify the area for `r=3` is approximately `28.274` (within 0.1% tolerance) and for `r=1` is approximately `3.14159`. Also test `r=0`.

import math
import pytest


def circle_area(r):
    pass  # implement here


def test_circle_area():
    pass
Solution
import math
import pytest


def circle_area(r):
    return math.pi * r ** 2


def test_circle_area_r3():
    assert circle_area(3) == pytest.approx(28.274, rel=1e-3)

def test_circle_area_r1():
    assert circle_area(1) == pytest.approx(3.14159, rel=1e-4)

def test_circle_area_r0():
    assert circle_area(0) == pytest.approx(0.0)
03

Assert List Contents

#

Write `top_scores(scores, n)` that returns the top `n` scores from a list, sorted in descending order. Write tests asserting the return type is a list, its length equals `n`, and specific values are present or absent.

def top_scores(scores, n):
    pass  # implement here


def test_top_scores():
    pass
Solution
def top_scores(scores, n):
    return sorted(scores, reverse=True)[:n]


def test_top_scores_returns_list():
    result = top_scores([5, 1, 8, 3, 9], 3)
    assert isinstance(result, list)

def test_top_scores_length():
    result = top_scores([5, 1, 8, 3, 9], 3)
    assert len(result) == 3

def test_top_scores_correct_values():
    result = top_scores([5, 1, 8, 3, 9], 3)
    assert 9 in result
    assert 8 in result
    assert 5 in result
    assert 1 not in result
04

Assert Dict Keys and Values

#

Write `parse_user(s)` that parses a string like `'name:Alice,age:30'` into a dict. Write tests asserting specific keys exist, values are correct, and unexpected keys are absent.

def parse_user(s):
    pass  # "name:Alice,age:30" → {'name': 'Alice', 'age': '30'}


def test_parse_user():
    pass
Solution
def parse_user(s):
    result = {}
    for pair in s.split(','):
        key, value = pair.split(':')
        result[key.strip()] = value.strip()
    return result


def test_parse_user_name():
    user = parse_user('name:Alice,age:30')
    assert 'name' in user
    assert user['name'] == 'Alice'

def test_parse_user_age():
    user = parse_user('name:Alice,age:30')
    assert 'age' in user
    assert user['age'] == '30'

def test_parse_user_no_extra_keys():
    user = parse_user('name:Alice,age:30')
    assert 'email' not in user
    assert len(user) == 2
05

Assert String Properties

#

Write `format_greeting(name, title)` that returns `'Dear {title}. {name},'`. Write tests asserting the result starts with 'Dear', ends with ',', contains the name, and has no double spaces.

def format_greeting(name, title):
    pass  # implement here


def test_format_greeting():
    pass
Solution
def format_greeting(name, title):
    return f'Dear {title}. {name},'


def test_greeting_starts_with_dear():
    result = format_greeting('Smith', 'Dr')
    assert result.startswith('Dear')

def test_greeting_ends_with_comma():
    result = format_greeting('Smith', 'Dr')
    assert result.endswith(',')

def test_greeting_contains_name():
    result = format_greeting('Smith', 'Dr')
    assert 'Smith' in result

def test_greeting_no_double_spaces():
    result = format_greeting('Smith', 'Dr')
    assert '  ' not in result