Skip to main content
Advertisement

parametrize — Parameterized Tests

@pytest.mark.parametrize runs a single test function repeatedly with multiple input values. You can cover diverse cases without duplicating code.


Basic Parameterization

import pytest


def add(a: int, b: int) -> int:
return a + b


# Single parameter
@pytest.mark.parametrize("value", [1, 2, 3, -1, 0])
def test_positive_check(value):
assert isinstance(value, int)


# Multiple parameters (grouped as tuples)
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, -2, -3),
(100, 200, 300),
])
def test_add(a, b, expected):
assert add(a, b) == expected


# Assign test names with ids
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
], ids=["positive", "zero", "cancel"])
def test_add_named(a, b, expected):
assert add(a, b) == expected

Parameterized Exception Testing

import pytest


def divide(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b


# Normal cases
@pytest.mark.parametrize("a, b, expected", [
(10, 2, 5.0),
(9, 3, 3.0),
(0, 5, 0.0),
])
def test_divide_ok(a, b, expected):
assert divide(a, b) == pytest.approx(expected)


# Exception cases
@pytest.mark.parametrize("a, b", [
(1, 0),
(100, 0),
(-5, 0),
])
def test_divide_zero(a, b):
with pytest.raises(ZeroDivisionError):
divide(a, b)


# Mixed normal/exception — use None to indicate no exception
@pytest.mark.parametrize("a, b, expected, exc", [
(10, 2, 5.0, None),
(0, 5, 0.0, None),
(1, 0, None, ZeroDivisionError),
])
def test_divide_mixed(a, b, expected, exc):
if exc:
with pytest.raises(exc):
divide(a, b)
else:
assert divide(a, b) == pytest.approx(expected)

Nested Parameterization (Cartesian Product)

import pytest


# All combinations of two parameters: 3 × 3 = 9 tests
@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", [10, 20, 30])
def test_multiply(x, y):
result = x * y
assert result == x * y
assert result > 0


# Parameterize operators
import operator


@pytest.mark.parametrize("op, a, b, expected", [
(operator.add, 1, 2, 3),
(operator.sub, 5, 3, 2),
(operator.mul, 4, 3, 12),
])
@pytest.mark.parametrize("scale", [1, 10])
def test_operation_scaled(op, a, b, expected, scale):
result = op(a * scale, b * scale)
assert result == expected * scale

pytest.mark — Test Marking

import pytest
import time


# skip: always skip
@pytest.mark.skip(reason="Not yet implemented")
def test_not_implemented():
assert False # never runs


# skipif: conditional skip
import sys


@pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows")
def test_unix_only():
assert True # skipped on Windows


# Version condition
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Python 3.10+ required")
def test_match_statement():
value = 42
match value:
case 42:
result = "answer"
case _:
result = "unknown"
assert result == "answer"


# xfail: expected failure (doesn't cause the whole suite to FAIL)
@pytest.mark.xfail(reason="Known bug #123")
def test_known_bug():
assert 1 + 1 == 3 # expected to fail → XFAIL


# xfail strict: FAIL if the test passes unexpectedly
@pytest.mark.xfail(strict=True, reason="Should still be broken")
def test_should_still_fail():
assert 1 + 1 == 3 # fails → XFAIL (passing → XPASS → FAIL)


# Custom marks
@pytest.mark.slow
def test_long_running():
time.sleep(0.1)
assert True


@pytest.mark.integration
def test_integration():
assert True

Combining Parameterization and Marks

import pytest
import sys


@pytest.mark.parametrize("input_val, expected", [
(0, True),
(1, False),
pytest.param(-1, False, id="negative"),
pytest.param(
999, True,
marks=pytest.mark.skip(reason="Edge case not verified"),
),
pytest.param(
"abc", None,
marks=pytest.mark.xfail(raises=TypeError),
),
])
def test_is_zero(input_val, expected):
assert (input_val == 0) == expected


# Apply conditional skip directly to parameters
@pytest.mark.parametrize("platform, expected", [
pytest.param("linux", True, marks=pytest.mark.skipif(
sys.platform != "linux", reason="Linux only"
)),
("win32", True),
("darwin", True),
])
def test_platform(platform, expected):
assert expected is True

Real-World Pattern: Input Validation Testing

import pytest
from dataclasses import dataclass


@dataclass
class User:
name: str
age: int
email: str

def __post_init__(self):
if not self.name:
raise ValueError("Name cannot be empty")
if not (0 <= self.age <= 150):
raise ValueError(f"Invalid age: {self.age}")
if "@" not in self.email:
raise ValueError(f"Invalid email: {self.email}")


# Valid cases
@pytest.mark.parametrize("name, age, email", [
("Alice", 30, "alice@example.com"),
("Bob", 0, "bob@test.org"),
("Charlie", 150, "c@d.io"),
])
def test_valid_user(name, age, email):
user = User(name=name, age=age, email=email)
assert user.name == name


# Invalid cases
@pytest.mark.parametrize("name, age, email, error_msg", [
("", 30, "a@b.com", "Name cannot be empty"),
("Alice", -1, "a@b.com", "Invalid age"),
("Alice", 151, "a@b.com", "Invalid age"),
("Alice", 30, "invalid", "Invalid email"),
])
def test_invalid_user(name, age, email, error_msg):
with pytest.raises(ValueError, match=error_msg):
User(name=name, age=age, email=email)

Summary

FeatureUsage
Basic parameterization@pytest.mark.parametrize("x", [1, 2, 3])
Multiple parameters@pytest.mark.parametrize("a, b", [(1, 2), (3, 4)])
Specify test IDids=["case1", "case2"] or pytest.param(..., id="name")
Nested parametersStack decorators → Cartesian product
Per-case markspytest.param(..., marks=pytest.mark.skip(...))
Skip@pytest.mark.skip / @pytest.mark.skipif
Expected failure@pytest.mark.xfail
Custom marks@pytest.mark.slow + register in pytest.ini markers

Parameterization is most powerful when validating the same logic across diverse inputs.

Advertisement