pytest and Python automation interview questions
Updated 2026-11-13 · 12 min read
Thirty pytest questions for Python automation roles: how tests are found, fixtures and their scope, parametrize, markers, conftest.py, API tests with requests, browser tests with pytest-playwright, reporting, parallel runs and running it all in CI.
pytest is the standard test runner for Python. Interviewers use it to check whether you can structure a suite, not just write one test. Examples below run against the free ShopLite practice app; the same tests are in its starter-tests-python folder.
The basics
1. Why pytest rather than unittest?
pytest tests are plain functions with plain assert statements, so there is little to write. It adds fixtures for set-up and clean-up, parametrize for data-driven tests, markers, clear failure output that shows the values on both sides of an assert, and a large plugin ecosystem. It also runs existing unittest tests, so a team can move gradually.
2. How does pytest find tests?
By naming convention: files called test_*.py or *_test.py, and inside them functions starting with test (and methods starting with test in classes starting with Test). You can change the rules in pytest.ini. Running pytest -v lists what it collected; pytest --collect-only shows the list without running anything.
3. How do you run one test, a file, or a group of tests?
pytest test_api.py # one file
pytest test_api.py::test_one_product_can_be_read # one test
pytest -k "cart and not checkout" # by name expression
pytest -m smoke # by marker
pytest -x # stop at the first failure
pytest --lf # only the ones that failed last time
4. How does an assertion failure look, and why is it useful?
pytest rewrites assert so the report shows the evaluated parts, not just "AssertionError".
def test_cart_total(api, demo_token):
cart = api("GET", "/api/cart", token=demo_token).json()
assert cart["total"] == 0
# E assert 1500 == 0
# E + where 1500 = {'items': [...], 'total': 1500}['total']
You can add a message (assert x == y, "total wrong after coupon") when the value alone does not explain the failure.
5. How do you check that code raises an error?
With pytest.raises, which fails the test if the exception does not happen.
import pytest
def test_negative_quantity_is_rejected():
with pytest.raises(ValueError, match="quantity"):
build_cart_line(product_id=3, qty=-1)
For floating-point results use pytest.approx(0.3) instead of ==.
Fixtures
6. What is a fixture?
A function that prepares something a test needs and hands it over, marked with @pytest.fixture. A test asks for it by naming it as a parameter; pytest runs it first and passes in what it returns.
@pytest.fixture
def demo_token(api):
response = api("POST", "/api/auth/login", json={"email": "[email protected]", "password": "Demo@1234"})
assert response.status_code == 200
return response.json()["token"]
def test_cart_is_empty(api, demo_token):
assert api("GET", "/api/cart", token=demo_token).json()["items"] == []
Fixtures can use other fixtures (here demo_token uses api), which builds up a test's setup piece by piece.
7. How do you do clean-up in a fixture?
Use yield: everything before it is set-up, everything after is tear-down, and it runs even if the test failed.
@pytest.fixture
def temp_user(api):
user = api("POST", "/api/auth/register", json={...}).json()
yield user # the test runs here
api("DELETE", f"/api/users/{user['id']}") # clean-up
This replaces setUp/tearDown and keeps the two halves next to each other.
8. What are fixture scopes?
How long a fixture lives before it is created again: function (the default, one per test), class, module, package and session (once for the whole run). Use a wide scope for expensive, read-only things, such as a logged-in API client or a started database, and function scope for anything a test can change.
@pytest.fixture(scope="session")
def base_url():
return os.environ.get("BASE_URL", "http://localhost:3001")
A fixture can only depend on fixtures with the same or a wider scope.
9. What is conftest.py?
A special file where you put fixtures and hooks that several test files share. pytest loads it automatically, so tests use the fixtures without importing anything. A conftest.py in a folder applies to that folder and below, so you can have a general one at the root and a specific one in a sub-folder.
10. What does autouse=True do, and when is it a good idea?
An autouse fixture runs for every test in its scope without being requested. The ShopLite tests use one to put the app back to its starting data before each test:
@pytest.fixture(autouse=True)
def fresh_app(base_url):
response = requests.post(f"{base_url}/api/_reset", timeout=10)
assert response.status_code == 204
It is good for things that must always happen (reset, logging). Overuse hides what a test depends on, so keep autouse to a few obvious ones.
11. How do fixtures differ from plain helper functions?
pytest manages a fixture: it controls order and scope, shares it across tests, runs clean-up reliably, shows it in --setup-show, and lets a test or a conftest.py override it. A helper function is just called by you. Use fixtures for resources with set-up and tear-down, and plain helpers for simple pure logic.
12. How do the built-in fixtures tmp_path, monkeypatch and capsys help?
tmp_path gives a clean temporary folder per test (for files a test writes). monkeypatch changes an environment variable, attribute or dictionary for one test and restores it afterwards. capsys captures what the code printed.
def test_reads_base_url_from_environment(monkeypatch):
monkeypatch.setenv("BASE_URL", "http://staging.example.test")
assert load_config().base_url == "http://staging.example.test"
Parametrize, markers and structure
13. What is parametrize, and why is it better than a loop?
It runs one test function once for each set of inputs and reports each as its own test, so one failing case does not hide the others.
@pytest.mark.parametrize("email", ["a@b", "no-at-sign.example.com", "two@@example.com"])
def test_malformed_email_is_rejected(api, email):
response = api("POST", "/api/auth/register", json={"name": "Asha Rao", "email": email, "password": "Passw0rd1"})
assert response.status_code == 400
A for loop inside a test stops at the first failure and reports one result. Give cases readable names with ids=[...] or pytest.param(..., id="empty").
14. How do you mark expected failures and skip tests?
@pytest.mark.skip(reason="feature not built yet")
def test_gift_cards(): ...
@pytest.mark.skipif(sys.platform == "win32", reason="uses a Unix path")
def test_path(): ...
@pytest.mark.xfail(reason="bug 1234: discount applied twice", strict=True)
def test_discount_once(): ...
xfail means "this is a known failure": it does not turn the build red. With strict=True it fails when the test unexpectedly passes, which tells you the bug was fixed and the marker should go.
15. What are markers and how do you register custom ones?
Labels you attach to tests so you can select them: @pytest.mark.smoke, @pytest.mark.api. Register them in pytest.ini so a typo is not silently ignored.
[pytest]
markers =
smoke: fast checks run on every pull request
bug: catches a planted ShopLite bug
addopts = -m "not bug" -ra --strict-markers
--strict-markers turns an unregistered marker into an error. Run pytest -m "smoke and not slow" to choose.
16. How do you organise a pytest project?
tests/
conftest.py shared fixtures
api/
conftest.py API client and token fixtures
test_cart.py
ui/
conftest.py browser and page fixtures
pages/ page objects
test_login.py
data/ test data files
pytest.ini
requirements.txt
Keep tests independent, keep one concern per file, and keep page objects and API helpers out of test files.
17. How do you share test data between tests?
Small data: parametrize or a fixture returning a dictionary. Larger data: files (CSV, JSON, YAML) loaded by a session-scoped fixture. Generated data: a factory fixture that returns a function so each test gets a fresh, unique value (for example a new email each time) without clashing with others. Never depend on data another test created.
@pytest.fixture
def new_email():
counter = itertools.count(1)
return lambda: f"tester{next(counter)}@example.test"
API testing with requests
18. How do you write an API test in pytest?
Call the endpoint, then assert on the status code, the headers that matter, and the body.
def test_adding_to_the_cart_updates_the_total(api, demo_token):
api("POST", "/api/cart/items", token=demo_token, json={"productId": 6, "qty": 2})
cart = api("GET", "/api/cart", token=demo_token).json()
assert cart["subtotal"] == 2 * 14900
assert cart["total"] == cart["subtotal"] - cart["discount"] + cart["tax"] + cart["shipping"]
Test the happy path, missing and wrong input, missing or wrong authorisation, and what the response should not contain. The API testing questions list what to cover.
19. How would you validate a JSON response against a schema?
Use jsonschema to check the shape (required fields, types) in one assertion, then assert the important values separately.
from jsonschema import validate
PRODUCT = {
"type": "object",
"required": ["id", "name", "price"],
"properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "price": {"type": "integer", "minimum": 0}},
}
def test_product_matches_schema(api):
validate(api("GET", "/api/products/3").json(), PRODUCT)
20. How do you keep API tests independent when they share one environment?
Reset or create the data inside the test or a fixture, give each test its own user or records, and clean up in the fixture. The ShopLite app has a reset endpoint for this; against a real system you create uniquely named data and delete it afterwards. Tests that need a particular order are tests waiting to fail under parallel runs.
Browser tests with pytest-playwright
21. How do you write a UI test with pytest-playwright?
Install pytest-playwright and run playwright install. The plugin gives you a page fixture: a fresh browser context for every test, closed afterwards.
from playwright.sync_api import Page, expect
def test_search_filters_products(page: Page):
page.goto("/#/products")
page.get_by_test_id("search").fill("backpack")
page.get_by_test_id("search-button").click()
expect(page.get_by_test_id("product-name")).to_have_text(["Backpack"])
page.goto("/...") works with a relative path because the plugin accepts --base-url (or base_url in pytest.ini).
22. Why use expect(...) instead of a plain assert for page state?
expect auto-retries until the condition is true or a timeout passes, so it waits for the page to catch up. A plain assert locator.inner_text() == "x" reads once and fails on slow rendering. Use expect for anything that depends on the page; plain assert for values you already have.
23. How do you choose the browser, run headed, or keep traces and videos?
Through command-line options of the plugin:
pytest --browser firefox --browser chromium # run in more than one browser
pytest --headed --slowmo 300 # watch it
pytest --tracing retain-on-failure --video retain-on-failure --screenshot only-on-failure
A trace from a failed CI run opens in playwright show-trace and shows each step, the page and the network, which is usually enough to find the cause without re-running.
24. How do you log in once instead of in every UI test?
Log in through the API or once in the UI, save the browser state, and reuse it. With the plugin you can override the browser_context_args fixture to load a saved state, or create a session-scoped fixture that logs in and writes state.json.
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
return {**browser_context_args, "storage_state": "state.json"}
It cuts minutes from a suite and removes the login page as a cause of failures in unrelated tests. Keep one test that really exercises login.
25. How do you get a page object into pytest tests?
Make the page object a fixture that receives page.
@pytest.fixture
def products(page):
pages = ProductsPage(page)
pages.open()
return pages
def test_search(products):
products.search("backpack")
expect(products.names).to_have_text(["Backpack"])
The test reads like the user's task and the locators live in one place. The page object returns a locator (products.names), not text, so the test can use expect and wait. Reading the text immediately can return the old list while the page is still updating.
Reports, speed and CI
26. How do you produce reports?
pytest --junitxml=results.xml writes JUnit XML that every CI tool reads. pytest-html makes a single HTML file (--html=report.html --self-contained-html). Allure gives richer reports with steps and attachments, at the cost of extra set-up. Whatever you choose, attach screenshots and traces for failures.
27. How do you run tests in parallel?
Install pytest-xdist and run pytest -n auto (one worker per CPU) or -n 4. Tests must be independent: no shared accounts, no order dependence, and no session fixture that mutates shared data. If you use a reset-everything fixture, as ShopLite's tests do, parallel runs against one app would reset each other's data; give each worker its own app instance or use data that is separate per test.
28. How do you retry flaky tests, and should you?
pytest-rerunfailures adds --reruns 2. Use it only as a short-term measure in CI, report the reruns, and fix the cause: a missing wait, shared data, a time dependency. A retry that hides a real intermittent bug in the product is worse than a red build. See finding and fixing flaky tests.
29. How do you run pytest in GitHub Actions?
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: starter-tests-python/requirements.txt
- run: pip install -r requirements.txt
working-directory: starter-tests-python
- run: python -m playwright install --with-deps chromium
- run: pytest --junitxml=results.xml
working-directory: starter-tests-python
Start the app first, wait until it answers, and upload the report and traces with if: always(). The complete, working workflow is in the Python tutorial.
30. A test passes alone but fails when the whole suite runs. What do you check?
Shared state. Look for a mutable module-level variable or default argument, a session-scoped fixture that tests change, data one test leaves behind, a fixture with too wide a scope, and tests that depend on running order (install pytest-randomly, which shuffles the order on every run, to expose them). Run the failing test with the one before it (pytest a.py::test_x a.py::test_y) to narrow it down, and use --setup-show to see which fixtures ran when.
Practise it
Clone the ShopLite app, run pytest in starter-tests-python, then run pytest -m bug to watch the bug tests fail on the buggy build, each failure being a planted bug found. Write a test for a bug of your own, and put the whole thing in a GitHub repository with the tutorial's workflow.
Sources
Want a project you can show in interviews?
The portfolio project kit walks you through testing a deliberately buggy app with Selenium or Playwright and API tests, then explaining it. The app itself is free.
