Python · Testing with pytest · Beginner

Parametrized Tests

5 tasks

Run one test function against multiple inputs with @pytest.mark.parametrize.

Stop Repeating Yourself: Parametrized Tests

#
**The problem:** When testing a function with multiple inputs, you'd normally write separate test functions: ```python def test_square_positive(): assert square(3) == 9 def test_square_negative(): assert square(-4) == 16 def test_square_zero(): assert square(0) == 0 ``` This is repetitive. If the `square` function changes, you update the same logic in multiple places. `@pytest.mark.parametrize` solves this by running one test function multiple times with different inputs: ```python import pytest @pytest.mark.parametrize("n, expected", [ (3, 9), (-4, 16), (0, 0), ]) def test_square(n, expected): assert square(n) == expected ``` Each row creates an independent test case — if `(3, 9)` fails, the others still run. pytest reports each separately. **Syntax breakdown:** ```python @pytest.mark.parametrize( "param_names", # comma-separated string of parameter names [ # list of values (or tuples for multiple params) value1, value2, ] ) def test_something(param_names): ... ``` For a single parameter: pass a flat list. For multiple parameters: pass a list of tuples, one per test case. **How test IDs are generated:** pytest auto-generates a name for each case from the parameter values: ``` test_square[3-9] ← from (3, 9) test_square[-4-16] ← from (-4, 16) test_square[0-0] ← from (0, 0) ``` You can override these with `ids=`: ```python @pytest.mark.parametrize("n, expected", [ (3, 9), (-4, 16), ], ids=["positive", "negative"]) # → test_square[positive], test_square[negative] ``` **When a parametrized test fails:** pytest shows exactly which parameter set caused it: ``` FAILED test_math.py::test_square[-4-16] AssertionError: assert 4 == 16 ``` You can then run just that failing case: ```bash pytest -k "test_square[-4-16]" ``` **When NOT to use parametrize:** If test cases have completely different logic — different assertions, different function calls — they should be separate test functions. Parametrize is for the same verification logic repeated across different inputs.

Parametrize Patterns

#
**Single parameter — flat list:** ```python import pytest @pytest.mark.parametrize("value", [-1, -100, -0.001]) def test_is_negative(value): assert value < 0 ``` **Multiple parameters — list of tuples:** ```python @pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), (0, 5, 5), (-1, 1, 0), (10, -3, 7), ]) def test_add(a, b, expected): assert a + b == expected ``` **Custom IDs for readability:** ```python @pytest.mark.parametrize("text, expected", [ ("hello world", "hello-world"), ("Python Tutorial", "python-tutorial"), (" spaces ", "spaces"), ], ids=["basic", "multi-word", "trim"]) def test_slugify(text, expected): assert slugify(text) == expected ``` Without custom IDs pytest would produce `test_slugify[hello world-hello-world]`, which is hard to read. With IDs: `test_slugify[basic]`. **Mixing truthy and falsy expected values:** ```python @pytest.mark.parametrize("s, expected", [ ("racecar", True), ("madam", True), ("hello", False), ("", True), # edge case: empty is a palindrome ]) def test_is_palindrome(s, expected): assert is_palindrome(s) == expected ``` **Stacking two parametrize decorators — cartesian product:** ```python @pytest.mark.parametrize("x", [1, 2]) @pytest.mark.parametrize("y", [10, 20]) def test_multiply(x, y): assert multiply(x, y) == x * y # generates: (1,10), (1,20), (2,10), (2,20) — 4 tests total ``` Use stacking when you want to test all combinations of two independent variables. **Edge cases as parametrize rows:** ```python @pytest.mark.parametrize("value", [None, "", 0, [], {}, False]) def test_falsy_values(value): assert not value ``` Listing edge cases as rows forces you to think about them explicitly — they become tracked test cases, not afterthoughts.

Parametrize Quick Reference

#
```python # Single parameter @pytest.mark.parametrize("name", [v1, v2, v3]) def test_something(name): ... # Multiple parameters (comma-separated string or list) @pytest.mark.parametrize("a, b, expected", [ (in1, in2, out1), (in3, in4, out2), ]) def test_something(a, b, expected): ... # Custom IDs @pytest.mark.parametrize("x", [1, 2, 3], ids=["one", "two", "three"]) def test_something(x): ... ``` **Combining with fixtures:** ```python @pytest.fixture def multiplier(): return 2 @pytest.mark.parametrize("n, expected", [(3, 6), (5, 10)]) def test_double(n, expected, multiplier): assert n * multiplier == expected ``` **Running a specific parameter set:** ```bash pytest -k "test_add[1-2-3]" # by auto-generated ID pytest -k "trim" # by custom ID substring ``` **Auto-generated ID format:** - Strings: value as-is (truncated if long) - Numbers: the number - `None`, `True`, `False`: their repr - Complex objects: `param0`, `param1`, ...
01

Parametrize square(n)

#

Write `square(n)` that returns `n * n`. Use `@pytest.mark.parametrize` to test it with at least 5 different inputs covering: a positive integer, a negative integer, zero, a float, and a large number.

import pytest


def square(n):
    pass


@pytest.mark.parametrize("n, expected", [
    # add 5 rows
])
def test_square(n, expected):
    assert square(n) == expected
Solution
import pytest


def square(n):
    return n * n


@pytest.mark.parametrize("n, expected", [
    (3,    9),
    (-4,   16),
    (0,    0),
    (0.5,  0.25),
    (100,  10000),
])
def test_square(n, expected):
    assert square(n) == expected
02

Parametrize is_palindrome

#

Write `is_palindrome(s)` that returns True if a string reads the same backwards. Use `@pytest.mark.parametrize` with at least 4 palindromes and 3 non-palindromes, including the expected boolean as a second parameter.

import pytest


def is_palindrome(s):
    pass


@pytest.mark.parametrize("s, expected", [
    # palindromes (True) and non-palindromes (False)
])
def test_is_palindrome(s, expected):
    assert is_palindrome(s) == expected
Solution
import pytest


def is_palindrome(s):
    return s == s[::-1]


@pytest.mark.parametrize("s, expected", [
    ("racecar",  True),
    ("madam",    True),
    ("level",    True),
    ("",         True),
    ("hello",    False),
    ("python",   False),
    ("abcba",    True),
    ("abcde",    False),
])
def test_is_palindrome(s, expected):
    assert is_palindrome(s) == expected
03

Parametrize with Two Arguments

#

Use `@pytest.mark.parametrize` to test `clamp(value, lo, hi)` with at least 5 cases covering: value within range, at lower boundary, at upper boundary, below lower bound, and above upper bound. Use four parameters per row: `value`, `lo`, `hi`, and `expected`.

import pytest


def clamp(value, lo, hi):
    return max(lo, min(value, hi))


@pytest.mark.parametrize("value, lo, hi, expected", [
    # add rows here
])
def test_clamp(value, lo, hi, expected):
    assert clamp(value, lo, hi) == expected
Solution
import pytest


def clamp(value, lo, hi):
    return max(lo, min(value, hi))


@pytest.mark.parametrize("value, lo, hi, expected", [
    (5,   0, 10, 5),
    (0,   0, 10, 0),
    (10,  0, 10, 10),
    (-5,  0, 10, 0),
    (15,  0, 10, 10),
    (-1,  0, 10, 0),
])
def test_clamp(value, lo, hi, expected):
    assert clamp(value, lo, hi) == expected
04

Add Custom IDs

#

The parametrized test below auto-generates unhelpful IDs like `test_parse_int[42-42]`. Add `ids=[...]` to the decorator with descriptive names. Run `pytest -v` before and after to see the difference.

import pytest


def parse_int(s):
    return int(s.strip())


@pytest.mark.parametrize("s, expected", [
    ("42",    42),
    ("  7 ",  7),
    ("-10",  -10),
    ("0",     0),
])
def test_parse_int(s, expected):
    assert parse_int(s) == expected
Solution
import pytest


def parse_int(s):
    return int(s.strip())


@pytest.mark.parametrize("s, expected", [
    ("42",    42),
    ("  7 ",  7),
    ("-10",  -10),
    ("0",     0),
], ids=["plain-integer", "with-whitespace", "negative", "zero"])
def test_parse_int(s, expected):
    assert parse_int(s) == expected
05

Parametrize Edge Cases for a Validator

#

Write `is_valid_username(s)` that returns True only if the string is 3–20 characters, contains only letters and digits, and does not start with a digit. Use `@pytest.mark.parametrize` to test at least 3 valid and 4 invalid usernames.

import pytest


def is_valid_username(s):
    pass  # implement the three validation rules


@pytest.mark.parametrize("username, expected", [
    # valid (True) and invalid (False) cases
])
def test_is_valid_username(username, expected):
    assert is_valid_username(username) == expected
Solution
import pytest


def is_valid_username(s):
    if not s or not (3 <= len(s) <= 20):
        return False
    if s[0].isdigit():
        return False
    return all(c.isalnum() for c in s)


@pytest.mark.parametrize("username, expected", [
    ("alice",       True),
    ("user123",     True),
    ("A1b2C3",      True),
    ("ab",          False),
    ("a" * 21,      False),
    ("1alice",      False),
    ("alice!",      False),
    ("",            False),
])
def test_is_valid_username(username, expected):
    assert is_valid_username(username) == expected