Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
297: Parametrizing Tests with pytest
Do I really have to write a separate test function for every single input?
Honestly? No. And if you find yourself copy-pasting a test function five times just to change the input value and the expected result, you're doing it the hard way. I've seen developers write hundreds of lines of redundant test code because they didn't know about parametrization.
In pytest, we use the @pytest.mark.parametrize decorator. It allows you to define one test "template" and then feed it a list of different values. Pytest will then run that test once for every single case you provided, treating them as individual tests in the final report.
import pytest
def is_strong_password(password):
return len(password) >= 8 and any(char.isdigit() for char in password)
@pytest.mark.parametrize("password, expected", [
("Short1", False),
("LongEnoughButNoDigit", False),
("CorrectPassword123", True),
("12345678", True),
])
def test_is_strong_password(password, expected):
assert is_strong_password(password) == expected
Notice how I passed a string "password, expected" to the decorator? Those names have to match the arguments in the function signature exactly. It's a clean way to separate your test logic from your test data.
How do I handle more than two arguments in my parametrization?
It's actually quite simple: you just keep expanding the comma-separated string and the corresponding tuples in your list. You aren't limited to just an input and an output; you can pass as many variables as your function needs to run.
Let's say we have a function that calculates a shipping cost based on weight, destination, and shipping speed. You'd set it up like this:
@pytest.mark.parametrize("weight, zone, speed, expected_cost", [
(1.0, "US", "Standard", 5.00),
(1.0, "US", "Express", 15.00),
(5.0, "EU", "Standard", 25.00),
(10.0, "EU", "Express", 50.00),
])
def test_shipping_calculator(weight, zone, speed, expected_cost):
assert calculate_shipping(weight, zone, speed) == expected_cost
One tip: if your list of test cases gets massive (like 50+ cases), I usually move that list into a separate constant or a JSON file to keep the test file from becoming a wall of data.
My test output is just a list of indices; how do I make it more readable?
By default, pytest labels parametrized tests by their index or the raw value, like test_is_strong_password[Short1-False]. That's fine for a few tests, but when a test fails in a CI/CD pipeline at 3 AM, you want to know exactly what scenario failed without hunting through the code.
You can fix this using the ids parameter. You can pass a list of strings that describe each case, or even a callable function that generates a name.
@pytest.mark.parametrize(
"password, expected",
[
("Short1", False),
("NoDigit", False),
("Valid123", True)
],
ids=["too_short", "missing_digit", "valid_password"]
)
def test_is_strong_password(password, expected):
assert is_strong_password(password) == expected
Now, when you run your tests, you'll see test_is_strong_password[too_short]. It turns your test suite into a form of documentation, which is a huge win for anyone else who has to maintain your code later.
📋 Practical Task
Exercise: Parametrizing a Discount Code Validator
You are tasked with testing a validate_discount_code function. This function takes a code (string) and a user_type (string) and returns a boolean indicating if the discount is applicable.
The Logic:
- 'SAVE10' is valid for all users.
- 'VIP20' is only valid for 'vip' users.
- 'NEW5' is only valid for 'new' users.
- Any other code is invalid.
Your Task:
Write a single test function using @pytest.mark.parametrize that covers at least 6 different scenarios (including successful applications and expected failures). You must use the ids parameter to clearly label each scenario (e.g., "vip_user_valid_code", "new_user_wrong_code").
# Starter code
def validate_discount_code(code, user_type):
if code == "SAVE10":
return True
if code == "VIP20" and user_type == "vip":
return True
if code == "NEW5" and user_type == "new":
return True
return False
# Your parametrized test goes here
There are no comments for now.