vcrpy review
VCR.py 8.3.0 intercepts HTTP calls made by supported Python clients, writes the real request and response to a YAML or JSON cassette, and replays matching interactions on later test runs. Version 8.3 adds niquests support, rejects recordings containing Python objects that the safe YAML loader cannot restore, provides an explicit custom-tag serializer, and fixes stale keep-alive connections crossing cassette boundaries. It is useful for realistic client integration tests, but the cassette is a stored wire conversation that can become stale or expose data if filters are incomplete.
VCR.py 8.3.0 installed in 0.4 seconds and occupied 4 MB across 3 packages in our sandbox, with 0 pip-audit findings; use it for a small set of expensive HTTP conversations that deserve realistic replay. Choose explicit mocks when the contract is short, the cassette would hold sensitive data, or the test must detect current upstream behavior.
We installed it
| Install | ✓ · 0.4s | 3 packages on disk · 4 MB |
| Import | ✓ | import vcr in 0.71s · pure Python · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does vcrpy install cleanly?
Yes. In a fresh container with an empty cache, pip install vcrpy finished in 0.4s, leaving 3 packages and 4 MB on disk. pip-audit reported no known vulnerabilities.
What does vcrpy need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import vcr succeeded in 0.71s.
vcrpy or responses: which should you use?
responses: Your code uses requests and concise hand-authored response mocks are easier to review than recordings. VCR.py 8.3.0 installed in 0.4 seconds and occupied 4 MB across 3 packages in our sandbox, with 0 pip-audit findings; use it for a small set of expensive HTTP conversations that deserve realistic replay.
When should you not use vcrpy?
Requests or responses contain credentials, personal data, cookies, signed URLs, or tokens you cannot safely store: filters must be correct before the first recording and do not clean old cassettes
Use it if
- You test a Python HTTP client against a slow, billed, rate-limited, or intermittently available third-party API
- A recorded status, header set, body, redirect, or error response is more informative than a hand-written mock
- Your team will review and commit cassette files as test fixtures, then refresh them when the upstream contract changes
- The code uses a transport VCR.py patches, such as requests, urllib3, HTTPX, aiohttp, boto3, or niquests
- Requests or responses contain credentials, personal data, cookies, signed URLs, or tokens you cannot safely store: filters must be correct before the first recording and do not clean old cassettes
- The test must prove today's provider behavior: replay returns an old response until someone records again, so an upstream breaking change can stay invisible
- Your contract is small enough to express directly: `responses`, `requests-mock`, `respx`, or `pytest-httpx` can produce shorter fixtures with fewer wire details
- Requests carry timestamps, signatures, random IDs, or order-sensitive bodies that change every run: custom normalization and matchers can cost more than an explicit fake
- Traffic leaves through an unsupported native client, subprocess, browser, or another service: VCR.py patches named Python HTTP stacks and cannot capture arbitrary network activity
- You receive cassette files from untrusted sources: 8.2.1 fixed arbitrary code execution from unsafe YAML tags, but treating executable-looking fixture data as trusted input is still a needless risk
Setup reality
Our fresh Python 3.12 install of VCR.py 8.3.0 succeeded in 0.4 seconds. It left 3 packages using 4 MB, reported 27 direct dependencies, imported as vcr in 0.71 seconds, and produced 0 known vulnerabilities in pip-audit. The distribution is pure Python, requires Python 3.10 or newer, uses the MIT license, and does not ship py.typed. PyYAML and wrapt are the ordinary runtime pieces; many HTTP clients appear only in test extras.
The first run makes a network request and writes a cassette. Under the default once mode, later matching calls replay, while a new unmatched call fails once the cassette exists. Use none in locked-down CI to forbid all new traffic, and reserve all or new_episodes for an intentional refresh with real credentials and quotas. The default matcher checks method, scheme, host, port, path, and query, but not headers or body. Add those only when they define the contract.
Scrub authorization headers, cookies, API keys in query strings, sensitive form fields, and secret response values before recording. Adding a filter tomorrow does not rewrite yesterday's cassette, so search existing fixtures separately. Version 8.3 fails before saving Python objects that its safe YAML loader cannot read. Register the new custom-tag serializer only when those objects are deliberate and every cassette source is trusted. Binary or compressed bodies can also make YAML files large and diffs hard to review.
Replay is a fixed interaction log rather than a stateful service. One recorded response is consumed once unless allow_playback_repeats is enabled, and concurrent requests can expose ordering assumptions. A transport VCR.py does not patch will still reach the network. Keep small unit tests around business decisions, use cassettes at the HTTP boundary, and schedule rerecording where upstream freshness matters. Recording may create remote data, spend quota, or trigger side effects, so treat refreshes as live integration runs.
Patterns
Record and replay one requests call record-request
import requests
import vcr
@vcr.use_cassette("tests/cassettes/user_42.yaml")
def test_fetch_user():
response = requests.get("https://api.example.com/users/42", timeout=10)
assert response.status_code == 200
assert response.json()["id"] == 42The first run can contact the real server; later runs replay only when the request matches the stored interaction.
Centralize cassette defaults configure-cassette-library
import vcr
api_vcr = vcr.VCR(
cassette_library_dir="tests/cassettes",
record_mode="once",
match_on=["method", "scheme", "host", "port", "path", "query"],
)
@api_vcr.use_cassette("health.yaml")
def test_health():
...These six matchers are VCR.py 8.3's defaults; headers and request bodies need explicit matchers when they affect the response.
Forbid new requests during replay block-network-in-ci
@api_vcr.use_cassette(
"user_42.yaml",
record_mode="none",
)
def test_user_offline():
response = client.fetch_user(42)
assert response.id == 42`record_mode='none'` raises for an unmatched request and guarantees that this cassette context does not create new HTTP traffic.
Rerecord every interaction deliberately refresh-cassette
with api_vcr.use_cassette(
"user_42.yaml",
record_mode="all",
):
client.fetch_user(42)`all` sends every request to the real service and overwrites recordings, so run it only with approved credentials, quota, and test data.
Remove common secrets before writing YAML filter-secrets
secure_vcr = vcr.VCR(
filter_headers=["authorization", "cookie", "set-cookie"],
filter_query_parameters=["api_key", "token"],
filter_post_data_parameters=["password"],
)Filters apply while recording; they do not scrub cassette files that were already written or committed.
Redact a sensitive response header scrub-response
import vcr
def scrub_response(response):
headers = response["headers"]
headers.pop("Set-Cookie", None)
headers.pop("set-cookie", None)
return response
api_vcr = vcr.VCR(before_record_response=scrub_response)Response callbacks receive cassette data before serialization; account for header casing used by the patched client.
Require the request body to match match-request-body
@api_vcr.use_cassette(
"create_user.yaml",
match_on=["method", "scheme", "host", "port", "path", "query", "body"],
)
def test_create_user():
...Body matching catches changed payloads but also rejects timestamps, random IDs, signatures, or unstable serialization unless you normalize them first.
Compare JSON bodies by parsed value register-custom-matcher
import json
import vcr
def same_json(left, right):
assert json.loads(left.body) == json.loads(right.body)
api_vcr = vcr.VCR()
api_vcr.register_matcher("json-body", same_json)
api_vcr.match_on = ["method", "uri", "json-body"]A custom matcher must raise `AssertionError` with a useful message when requests differ; malformed or empty bodies need their own handling.
Replay one recorded response more than once repeat-response
with api_vcr.use_cassette(
"feature_flag.yaml",
allow_playback_repeats=True,
):
assert client.flag("checkout") is True
assert client.flag("checkout") is TrueVCR.py normally consumes each matching interaction once; repeated playback can hide an accidental duplicate request, so enable it narrowly.
Assert what the test sent inspect-cassette
with api_vcr.use_cassette("search.yaml") as cassette:
client.search("blue widgets")
assert len(cassette.requests) == 1
assert cassette.requests[0].method == "GET"
assert "q=blue+widgets" in cassette.requests[0].uriCassette requests appear in processing order, which makes order assertions brittle when the code starts sending work concurrently.
Replay an asynchronous HTTPX call test-async-httpx
import httpx
import vcr
@vcr.use_cassette("tests/cassettes/status.yaml")
async def test_async_status():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/status")
assert response.status_code == 200VCR.py 8.2 moved HTTPX patching to transports and added HTTPX 2 support; test against the exact HTTPX release in your environment.
Remove interactions the test no longer uses drop-unused-recordings
with api_vcr.use_cassette(
"workflow.yaml",
record_mode="new_episodes",
drop_unused_requests=True,
):
client.current_step()`drop_unused_requests` rewrites the cassette without interactions that were not replayed, so review the resulting fixture diff before committing it.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| responses | PyPI | Your code uses requests and concise hand-authored response mocks are easier to review than recordings |
| requests-mock | PyPI | You want a requests adapter or pytest fixture with programmatic matching |
| respx | PyPI | HTTPX route mocks and async-aware assertions matter more than recording live traffic |
| pytest-httpx | PyPI | You want pytest-native HTTPX responses with strict checks for unused mocks |
More testing guides
pytest · chai · jsdom · vitest · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

