Use @pytest.mark.parametrize to run one test body against a list of hostile inputs. Each input becomes its own case with its own name, so a failure points at the exact payload, and adding a new attack string takes one line instead of a new test function.
How parametrize works
The pytest documentation says the marker takes argument names and a list of value sets, and the test runs once per set. Each case gets an automatic ID built from its values. Stacked decorators produce all combinations, so two decorators with two values each run four cases. The marker can sit on a function, a class or a whole module.
For security tests this gives a clean pattern: the behavior is written once, the data lives in a table.
A pattern for input validation
Suppose an endpoint should reject certain values for a name field. Write the rule once:
import pytest
HOSTILE = [
"' OR '1'='1",
"../../etc/passwd",
"A" * 10000,
"",
"name\x00.txt",
]
@pytest.mark.parametrize("value", HOSTILE)
def test_name_field_rejects_hostile_input(client, value):
response = client.post("/profile", json={"name": value})
assert response.status_code in (400, 422)
Each string runs separately. If the long string fails but the others pass, the report shows which one.
Tips that save time
- Name cases clearly. A readable ID beats a wall of escaped characters in a failure message.
- Group by bug class. One list for injection strings, one for path tricks, one for size limits.
- Keep the expected outcome with the data when it differs per case, using tuples.
- Keep the lists in a separate module so security and QA can both review and extend them.
- Link each unusual value to the incident or ticket that introduced it, in a comment.
Watch the shared state trap
The docs warn that parameter values are passed as is, not copied. If a test mutates a list or dictionary parameter, later cases see the change. A test that edits a shared payload can quietly weaken every case that follows. Build fresh objects inside the test, or use immutable values like tuples and strings.
Combining with other markers
| Need | Approach |
|---|---|
| One input still works, fix scheduled | Wrap it with pytest.param and an xfail mark |
| Cover role and input together | Stack two decorators for all combinations |
| Same rule on every endpoint | Parametrize over a list of paths |
| Only on some platforms | Add a skip condition with a reason |
The docs show that marking one case with xfail reports it as xfailed while the rest pass, which keeps the known gap visible without breaking the suite.
What this does not replace
A fixed list only checks what you thought of. Pair it with a fuzzer or property based test that generates inputs you did not think of, and feed anything those tools find back into the list as a permanent case. The list grows from real failures, which is what makes it valuable over time.
Also check the response body and side effects, not only the status code. A 400 that still wrote a row to the database is a failure.
Try it yourself
Click through it at your own pace, or open it full screen. See how one scan exercises many endpoints and parameters, then compare with your own hostile input table.
Safeguard's free scan covers dependency and SBOM findings. Input tests for your own endpoints stay in your suite.
Quick check
Does each parametrized case show up separately?
Yes. Each value set runs as its own test with its own ID, so failures name the input.
Can parameters leak between cases?
Yes, because values are not copied. Avoid mutating shared lists or dictionaries inside the test.
Sources
Last reviewed: 19 October 2026.