PyTest
The definitive Python testing framework. Powerful fixtures, parameterised tests, and a plugin ecosystem that covers every testing need.
Overview
PyTest is the most popular testing framework for Python. Unlike Python's built-in unittest module, PyTest uses a simple assert statement (no special assertion methods needed) and provides powerful features like fixtures, parameterisation, and plugins. PyTest can run unittest, nose, and doctest-style tests, making it a universal test runner for Python projects.
PyTest's fixture system is particularly powerful — it allows test dependencies (databases, API clients, temporary files) to be injected automatically, with setup and teardown handled declaratively. This makes complex integration tests clean and maintainable.
What it's used for
PyTest is essential for:
- Python unit testing: The standard choice for testing Python code.
- Integration testing: Fixtures make database, API, and service testing elegant.
- API testing: Combined with requests or httpx, PyTest is a powerful API test framework.
- Data-driven testing: Parameterise tests with multiple data sets effortlessly.
Pros & Cons
Pros
- Simple assert syntax — no special assertion methods
- Powerful fixture system for dependency injection
- Excellent parameterisation for data-driven tests
- Massive plugin ecosystem (coverage, HTML reports, parallel execution)
- Can run unittest, nose, and doctest tests
Cons
- Python only — not suitable for other languages
- Fixture hierarchy can become complex in large projects
- Debugging fixture issues requires understanding of scope and resolution
- Some plugins have compatibility issues with newer PyTest versions
- Slower than unittest for very large test suites (though plugins can help)
Platforms & Integrations
PyTest runs on Windows, macOS, and Linux. It requires Python 3.7+. It integrates with all Python build tools and CI platforms.
Pricing
| Tier | Cost | Includes |
|---|---|---|
| Open Source | Free | Full framework, all core features, community support |
NZ Context
PyTest is the standard testing framework in NZ Python teams. Every NZ Python developer job posting lists PyTest as a required or preferred skill. Figured, Vend, and NZ data science teams use PyTest extensively. For NZ testers working with Python backends or data pipelines, PyTest is essential.
Alternatives
- unittest (Python stdlib) — Built-in but less powerful. PyTest can run unittest tests.
- nose2 — Successor to nose. Less popular than PyTest.
- Robot Framework — Keyword-driven testing for teams that want readable tests.
When to choose pytest
A quick decision guide for NZ teams evaluating python testing options.
| Choose pytest when… | Choose something else when… | Combine with… |
|---|---|---|
| Your backend is Python — Django, FastAPI, Flask, or a data pipeline | The codebase is JavaScript or TypeScript — use Vitest or Jest instead; fighting the language boundary is not worth it | pytest-cov for coverage thresholds enforced in CI; blocks merges below your agreed floor |
| You need data-driven tests — many input/output combinations against the same logic | Non-technical stakeholders need to read or write the tests — Robot Framework's keyword syntax is more accessible than Python | pytest-xdist to parallelise across CPU cores; essential once your suite passes ~5 minutes |
| You are testing a REST or GraphQL API with complex auth flows where fixtures can manage token lifecycle cleanly | You need exploratory or manual-assist tooling — Postman or Bruno are better fits for ad-hoc HTTP inspection | responses or respx to mock HTTP calls in unit tests; keeps tests fast and offline-safe |
| You are inheriting a legacy unittest suite and want to modernise incrementally — pytest runs it as-is | Your team is primarily manual testers with no Python background — the learning curve is real; invest in skilling up or choose a lower-friction tool first | Playwright for Python for end-to-end browser tests that share the same fixture patterns and CI pipeline as your unit suite |
What I would do
Practitioner judgment on tool adoption, team onboarding, and when to swap.
If…
I was joining a QA team at CloudBooks supporting a Python microservices backend with patchy coverage and a mix of legacy unittest tests and newer ad-hoc scripts
I would…
Introduce pytest as the runner first, before touching test content — it runs the existing unittest suite unchanged, so there is zero disruption. Then convert the ad-hoc scripts one file at a time using a conftest.py fixture to manage shared state (database seeds, auth tokens). Add pytest-cov with a modest starting threshold (60%) enforced in GitHub Actions, and raise it by 5% each sprint. This gives the team an observable, incremental improvement story rather than a risky big-bang rewrite. I would not touch Robot Framework here — CloudBooks's engineers write Python fluently and the extra abstraction layer adds ceremony without value.
If…
I was leading test automation at Revenue NZ where a large data transformation pipeline written in Python feeds tax calculations, and the team is under pressure from Revenue NZ's assurance team to demonstrate regression safety after each fortnightly release
I would…
Build a parameterised pytest suite using @pytest.mark.parametrize with golden-file test data — each known input/output pair stored in a versioned CSV or JSON fixture file. This makes adding new edge cases a data-entry task, not a coding task, which the business analyst can own. I would enable pytest-html to generate a self-contained HTML report that the assurance team can download from Azure DevOps without needing access to the codebase. pytest-xdist would run the suite in parallel to stay under the 10-minute CI gate. The critical difference from ad-hoc scripting: every failure produces a diff of expected vs actual output, not just a traceback, which makes triage far faster.
If…
I was a solo QA engineer at ListRight asked to evaluate whether to replace a Postman collection with automated API tests, with a Python backend already in place
I would…
Port the Postman collection to pytest + httpx, not to a Postman-to-code generator. Generators produce unmaintainable output. Instead, I would write a session-scoped fixture that authenticates once and shares the token across all tests, then group tests by resource (listings, search, payments) using pytest markers. I would keep the Postman collection alive for exploratory use — it is not either/or. The pytest suite owns regression; Postman owns discovery. I would resist suggestions to use Karate or RestAssured here: the Python backend team already reviews Python PRs, so keeping tests in the same language means developers can read and contribute to the test suite rather than treating it as a QA black box.
The bottom line: Pytest earns its place the moment you have more than twenty tests — before that, plain unittest is fine. After that point, the fixture system and parameterisation pay back their learning cost within a single sprint, and you will never go back to managing setUp/tearDown boilerplate by hand.
Interview questions
Questions you are likely to get if you list pytest on your CV — with what interviewers are really testing for.
What is the difference between a pytest fixture and a setUp/tearDown method in unittest?
What they’re really testing: Whether you understand pytest’s dependency injection model, not just that you have used both.
Strong answer covers: Fixtures are injected by name into test functions (not inherited from a class), allowing multiple fixtures to compose cleanly; scope options (function, class, module, session) let you control lifetime declaratively; contrast with setUp which always runs per method with no scoping — show you know yield fixtures for teardown and can explain why conftest.py enables sharing across files.
When would you choose pytest-xdist over running tests sequentially, and what trade-offs does it introduce?
What they’re really testing: Whether you have hit real-world parallelism problems — shared state, database isolation, flaky ordering — and know how to manage them.
Strong answer covers: Use xdist when the suite exceeds ~5 minutes in CI; trade-offs include tests that share a writable database needing isolation per worker (e.g. separate test schemas or transaction rollback per worker); session-scoped fixtures become trickier; mention -n auto vs pinning worker count, and that some plugins (e.g. pytest-html) need the --dist flag to aggregate reports correctly.
You’re joining TeleNZ’s backend QA team. Their Python microservices suite has 800 tests and no coverage enforcement. How would you introduce pytest-cov without disrupting the team?
What they’re really testing: Practical change-management sense alongside technical knowledge — can you introduce tooling incrementally without triggering resistance or breaking CI.
Strong answer covers: Start by running coverage in report-only mode (no --cov-fail-under) to establish a baseline; add the threshold at the current floor so no existing PR fails; raise by 5% per sprint via team agreement; use .coveragerc to exclude generated code and migrations that inflate the numbers; generate an HTML report so developers can see uncovered branches visually rather than just a percentage.
Your pytest suite passes locally but fails intermittently in GitHub Actions CI. How do you diagnose and fix it?
What they’re really testing: Systematic debugging instinct and familiarity with common CI/local environment mismatches.
Strong answer covers: Check for test-order dependency first using pytest --randomly-seed=last or pytest-randomly; look for race conditions in session-scoped fixtures under xdist; verify environment variables and secrets are available in the CI context; compare Python and dependency versions (pip freeze vs requirements.txt); use pytest -v --tb=long and artifact the full log; add pytest-rerunfailures as a short-term diagnostic tool to confirm intermittency, not as a permanent fix.
How would you structure a pytest suite for a FastAPI service with 15 endpoints, shared auth, and a PostgreSQL database, so it stays maintainable as the team grows?
What they’re really testing: Whether you think architecturally about test code — separation of concerns, reuse patterns, and keeping the suite readable for engineers who didn’t write it.
Strong answer covers: One conftest.py at the root for shared fixtures (test database, authenticated HTTP client via httpx.AsyncClient, factory functions for creating test data); separate test files per resource group (e.g. test_listings.py, test_payments.py); session-scoped fixture for the database that runs migrations once, then a function-scoped fixture that wraps each test in a transaction and rolls back; use pytest markers (@pytest.mark.integration, @pytest.mark.unit) to allow fast unit-only runs in pre-commit hooks and full integration runs in CI.