111. How would you use pytest parametrization to test many inputs without duplicating test code?
Explain @pytest.mark.parametrize, multiple parameters, readable case identifiers, expected exceptions, edge cases, and when separate tests are clearer than one large parameterized test.
I use pytest.mark.parametrize when the same behavior should be checked with several inputs and expected results. I keep one clear test body, pass multiple parameters when the case needs them, and give important cases readable ids. For expected exceptions, I include the expected exception in the case data and use pytest.raises only for those cases. The tradeoff is that a large parameter table can hide intent, so I use separate tests when cases need different setup, assertions, or business explanations.
See the Code while reading this explanation.
The practical decision is to parametrize only cases that follow the same test flow. The unit under test stays the same, the setup is similar, the action is the same, and the assertion shape is the same. Only the inputs and expected outcomes change.
- What behavior and test boundary should I cover?
- Which dependencies, environments, and test tools should I assume?
- Which failures, edge cases, and quality risks are most important?
With pytest.mark.parametrize, I define parameter names and provide a list of cases. Pytest creates one independent test case for each row. This removes repeated test functions while still reporting each case separately. For example, a discount function can be tested with an amount, a customer type, and an expected total. The test body calls the function once and compares the result with the expected value.
Multiple parameters are useful when the behavior depends on more than one value. The parameter names should describe the business meaning, such as amount, customer_type, and expected_total. I avoid vague names such as a, b, and result because they make failures harder to understand.
Readable case identifiers help when the raw parameter values are not clear. I can use ids with short labels such as regular_customer, vip_customer, zero_amount, and boundary_amount. Another option is pytest.param with id for each case. The id should explain why the case exists, not repeat every input value.
Expected exceptions need a clear pattern. If only a few cases should fail, I can store the expected exception type with each case. The test uses pytest.raises for exception cases and a normal equality assertion for success cases. I keep success and failure handling explicit so the reader can see the intended behavior. When success and exception cases need very different setup or assertions, separate tests are usually clearer than one complicated parameterized test.
Edge cases should come from the real contract of the function. Typical examples include zero, empty input, minimum and maximum allowed values, invalid types when the function validates them, and values directly around a business boundary. I do not add random cases without a reason. Each row should represent a distinct behavior or risk.
Parametrized cases should remain deterministic and independent. The test should not mutate shared input objects or depend on execution order. If a case uses a list, dictionary, or custom object that the function may modify, I create fresh data for each case with a factory or fixture. Pytest reports each parameter set separately in local runs and CI, which makes failures easier to locate.
A large parameterized test becomes misleading when rows test different behaviors, require many optional fields, or use branching logic to choose different assertions. At that point, I split the cases into smaller parameterized tests or separate named tests. Parametrization should reduce duplication without hiding the purpose of the test.
- Identify one behavior that should work for several inputs.
- Confirm that every case uses the same setup, action, and assertion shape.
- Choose clear parameter names.
- Create cases with inputs and expected outcomes.
- Add readable ids for important or non obvious cases.
- Use pytest.raises for cases that expect an exception.
- Add boundary and edge cases from the real function contract.
- Keep each case independent and deterministic.
- Split the test when cases require different setup, actions, or assertions.
- Run the cases in CI and use the case ids to diagnose failures.
The example tests a calculate_shipping function with several order totals and customer types. The first test uses multiple parameters and readable ids for successful cases. The second test parametrizes invalid inputs and verifies the expected exception with pytest.raises. Each case follows one clear test flow, and success and exception cases are separated because their assertions are different.
import pytest
def calculate_shipping(order_total: int, customer_type: str) -> int:
if order_total < 0:
raise ValueError("order_total must be zero or greater")
if customer_type not in {"regular", "vip"}:
raise ValueError("unsupported customer type")
if customer_type == "vip" or order_total >= 5000:
return 0
return 500
@pytest.mark.parametrize(
("order_total", "customer_type", "expected_shipping"),
[
pytest.param(0, "regular", 500, id="zero-total"),
pytest.param(4999, "regular", 500, id="below-free-shipping"),
pytest.param(5000, "regular", 0, id="free-shipping-boundary"),
pytest.param(1000, "vip", 0, id="vip-customer"),
],
)
def test_calculate_shipping(
order_total: int,
customer_type: str,
expected_shipping: int,
) -> None:
result = calculate_shipping(order_total, customer_type)
assert result == expected_shipping
@pytest.mark.parametrize(
("order_total", "customer_type", "expected_message"),
[
pytest.param(
-1,
"regular",
"order_total must be zero or greater",
id="negative-total",
),
pytest.param(
1000,
"unknown",
"unsupported customer type",
id="unsupported-customer-type",
),
],
)
def test_calculate_shipping_rejects_invalid_input(
order_total: int,
customer_type: str,
expected_message: str,
) -> None:
with pytest.raises(ValueError, match=expected_message):
calculate_shipping(order_total, customer_type)Pytest parametrization is used for validation rules, parsing functions, pricing logic, permission checks, date calculations, API input validation, and algorithms with many boundary cases. It is especially useful when the same public behavior must be checked against a table of inputs and expected outputs.
Interviewers ask this to see whether the candidate can organize many input cases without copying the same test body. They are evaluating knowledge of pytest parametrization, readable case design, exception testing, edge case coverage, and the judgment to split tests when one parameter table becomes difficult to understand.
A common mistake is placing unrelated behaviors in one parameter table. Another is adding branching logic inside the test so every row follows a different path. Other mistakes include unclear parameter names, missing case ids, sharing mutable data between cases, repeating the same case with different values but no new behavior, mixing success and exception assertions in a confusing way, and creating a very large table that is harder to understand than several focused tests.
Explain that parametrization is best when the setup, action, and assertion shape stay the same. Mention multiple parameters, readable ids, explicit exception cases, real edge cases, and the point where separate tests become clearer.










