pytest Parametrize
Basic @pytest.mark.parametrize
import pytest
# Single parameter
@pytest.mark.parametrize("n,expected", [
(1, 1),
(5, 120),
(10, 3628800),
])
def test_factorial(n, expected):
assert factorial(n) == expected
# Multiple parameters — tuples
@pytest.mark.parametrize("input_str,expected", [
("hello", "HELLO"),
("World", "WORLD"),
("", ""),
("123abc", "123ABC"),
])
def test_to_upper(input_str, expected):
assert input_str.upper() == expected
# Single list of values
@pytest.mark.parametrize("email", [
"user@example.com",
"name+tag@domain.co.uk",
"dots.allowed@example.org",
])
def test_valid_email(email):
assert is_valid_email(email) is True
ids — Custom Test Names
import pytest
# Explicit string IDs
@pytest.mark.parametrize("value,expected", [
(0, True),
(1, False),
(-1, False),
], ids=["zero", "positive", "negative"])
def test_is_zero(value, expected):
assert (value == 0) == expected
# pytest.param for per-case metadata
@pytest.mark.parametrize("config", [
pytest.param({"debug": True}, id="debug-mode"),
pytest.param({"debug": False}, id="prod-mode"),
pytest.param({"debug": True, "verbose": True}, id="full-debug"),
])
def test_app_config(config):
app = create_app(config)
assert app.configured
# IDs from callable
@pytest.mark.parametrize("obj", [
pytest.param(User(id=1), id="user-1"),
pytest.param(Admin(id=2), id="admin-2"),
])
indirect — Parametrize Through Fixtures
import pytest
@pytest.fixture
def user_factory(request):
"""Fixture that receives parametrize value via request.param."""
role = request.param
return create_user(role=role, name=f"{role}_user")
@pytest.mark.parametrize("user_factory", ["admin", "editor", "viewer"],
indirect=True)
def test_user_dashboard(user_factory):
user = user_factory # already created by fixture
response = client.get("/dashboard", user=user)
assert response.status_code == 200
# Partial indirect — only some params go through fixtures
@pytest.fixture
def db_conn(request):
return connect(request.param)
@pytest.mark.parametrize("db_conn,query", [
("postgres", "SELECT 1"),
("sqlite", "SELECT 1"),
], indirect=["db_conn"])
def test_basic_query(db_conn, query):
result = db_conn.execute(query).fetchone()
assert result is not None
Matrix Testing — Stacked parametrize
import pytest
# Stacking creates cartesian product (2x3 = 6 test cases)
@pytest.mark.parametrize("browser", ["chrome", "firefox"])
@pytest.mark.parametrize("resolution", ["1920x1080", "1366x768", "375x812"])
def test_layout(browser, resolution):
# Runs 6 times: chrome/1920, chrome/1366, chrome/375,
# firefox/1920, firefox/1366, firefox/375
driver = setup_driver(browser, resolution)
assert driver.page_renders_correctly()
# Combined with marks
@pytest.mark.parametrize("status_code,should_retry", [
pytest.param(500, True, id="server-error"),
pytest.param(429, True, id="rate-limit"),
pytest.param(404, False, id="not-found"),
pytest.param(200, False, marks=pytest.mark.skip(reason="skip success"), id="success"),
])
def test_retry_logic(status_code, should_retry):
result = handle_response(MockResponse(status_code))
assert result.should_retry == should_retry
Combining parametrize with Fixtures
import pytest
@pytest.fixture
def api_client(app):
return app.test_client()
# Use both fixtures AND parametrize in the same test
@pytest.mark.parametrize("endpoint,expected_status", [
("/health", 200),
("/api/v1/users", 200),
("/nonexistent", 404),
("/api/v1/admin", 403),
])
def test_endpoints(api_client, auth_headers, endpoint, expected_status):
response = api_client.get(endpoint, headers=auth_headers)
assert response.status_code == expected_status
# Dynamic parametrize from external data
def load_test_cases():
with open("tests/fixtures/cases.json") as f:
return [(c["input"], c["expected"]) for c in json.load(f)]
@pytest.mark.parametrize("input_data,expected", load_test_cases())
def test_processor(input_data, expected):
assert process(input_data) == expected
Marks with parametrize
| Mark | Usage | Effect |
|---|---|---|
pytest.param(..., marks=pytest.mark.skip) | Skip a specific case | Case skipped unconditionally |
pytest.mark.skipif | Conditional skip | Skip based on condition |
pytest.param(..., marks=pytest.mark.xfail) | Expected failure | Test passes if it fails |
pytest.mark.slow | Custom marker | Selectable with -m slow |
@pytest.mark.parametrize("n", [
pytest.param(0, marks=pytest.mark.xfail(raises=ZeroDivisionError)),
pytest.param(1),
pytest.param(100),
])
def test_divide(n):
assert 100 / n > 0