vcrpy
VCR.py records real HTTP requests and responses into cassette files, then intercepts matching calls in later test runs and replays the stored responses. It gives integration-style client tests deterministic network behavior without hand-writing every mock response. The trade is that cassettes become test fixtures containing protocol details, data, and sometimes secrets, so they need deliberate matching, filtering, and review.
VCR.py is excellent for preserving a few costly, realistic HTTP interactions, provided cassettes are treated as sensitive source files and refreshed on purpose. Prefer explicit mocks when the useful contract is small or when replay would conceal the behavior you actually need to test.
Use it if
- You test an HTTP client against a third-party API and want realistic response bodies, headers, status codes, and redirects without live calls in CI
- The external API is slow, rate-limited, billed, or intermittently unavailable but a recorded interaction remains meaningful
- You are prepared to commit cassettes as reviewed fixtures and refresh them when the upstream contract changes
- Your code uses one of VCR.py's supported Python HTTP stacks and request replay is more useful than a hand-authored behavioral fake
- The response contains credentials, personal data, signed URLs, or session identifiers you cannot safely commit: filters are configurable but omission is easy and existing cassette files remain sensitive
- You need tests to prove the provider's current production behavior: replay deliberately hides upstream changes until someone deletes or rerecords the cassette
- Your assertions should describe a small business contract rather than a full wire response: responses or respx produce shorter fixtures that reviewers can understand directly
- Requests contain volatile signatures, timestamps, random IDs, or unordered bodies that are hard to match; custom matchers can fix this, but they add another test framework inside the test
- Your transport is unsupported or heavily customized: VCR.py patches specific HTTP libraries and cannot intercept arbitrary native clients, subprocess traffic, browser requests, or another service's network calls
Setup reality
pip install vcrpy installs PyYAML and wrapt; 8.3.0 requires Python 3.10 or newer. The first run is intentionally different from later runs: the default once mode records when the cassette is absent, then rejects unmatched network calls when it exists. That surprises developers who change a request and expect the fixture to update. Pick a cassette directory and naming convention, commit the files, and make CI use none or once rather than all. Request matching defaults are not a complete API contract, so choose whether headers, query strings, body, and host must match. Filter Authorization, cookies, API keys, tokens in query parameters, and sensitive response fields before the first recording; adding a filter later does not scrub old files. Cassettes can also contain large binary or compressed bodies, and YAML diffs are noisy. Rerecording is a real external action that needs credentials, network access, test data cleanup, and often API quotas. The recorded response can become semantically stale even when matching still succeeds, so schedule refreshes for contracts that change. Async support depends on the actual client integration, and code that bypasses the patched library will still use the network. A cassette is not a service simulator: it replays a fixed sequence of interactions, stateful workflows can require repeated responses, and concurrency can make ordering assumptions visible. Keep unit tests around business decisions and use VCR.py for the narrower boundary where preserving a real HTTP conversation adds confidence.
Patterns
Record and replay a test with a decoratorrecord-test-decorator
import requests
import vcr
@vcr.use_cassette('tests/cassettes/user.yml')
def test_user():
response = requests.get('https://api.example.com/users/42')
assert response.json()['id'] == 42The first run can use the real network; later runs replay only a matching request from the cassette.
Limit cassette patching to one blockrecord-context-manager
with vcr.use_cassette('tests/cassettes/search.yml') as cassette:
response = requests.get('https://api.example.com/search', params={'q': 'maps'})
assert response.status_code == 200
assert cassette.play_count == 1Network interception applies only inside the context, which makes accidental real calls outside it easier to spot.
Create a project-level VCR configurationconfigure-cassette-library
import vcr
my_vcr = vcr.VCR(
cassette_library_dir='tests/cassettes',
record_mode='once',
match_on=['method', 'scheme', 'host', 'port', 'path', 'query'],
)
@my_vcr.use_cassette('list-users.yml')
def test_list_users():
...Per-cassette options override this object; keep the shared matching policy in one place so fixtures behave consistently.
Forbid new recordingsblock-network-in-ci
with my_vcr.use_cassette('users.yml', record_mode='none'):
response = requests.get('https://api.example.com/users')none mode fails on a request with no matching interaction, making it the safest CI setting when live network access is forbidden.
Rerecord every interaction deliberatelyrefresh-existing-cassette
with my_vcr.use_cassette('users.yml', record_mode='all'):
response = requests.get('https://api.example.com/users')all mode always uses the real network and appends recordings; use it only in a controlled refresh workflow with safe credentials and test data.
Remove authorization before writing YAMLfilter-auth-header
safe_vcr = vcr.VCR(
filter_headers=[('authorization', 'REDACTED')],
filter_cookies=['sessionid'],
)Configure filters before the first recording and inspect the resulting cassette; this does not sanitize files already on disk.
Redact API keys in query parametersfilter-query-secret
safe_vcr = vcr.VCR(
filter_query_parameters=[('api_key', 'REDACTED')],
)Filtering changes the stored request used for later matching, so apply the same configuration every time the cassette is replayed.
Skip recording selected requestsignore-health-checks
def before_record_request(request):
if request.path == '/health':
return None
return request
my_vcr = vcr.VCR(before_record_request=before_record_request)Returning None drops the request from recording; a real call can still occur while a cassette is being recorded.
Modify a response before it reaches the cassettescrub-response-body
import json
def scrub_response(response):
body = json.loads(response['body']['string'])
body.pop('access_token', None)
response['body']['string'] = json.dumps(body).encode()
return response
my_vcr = vcr.VCR(before_record_response=scrub_response)Response bodies may be bytes and may not be JSON; make the hook defensive if the cassette covers multiple endpoints.
Require request bodies to matchmatch-json-body
strict_vcr = vcr.VCR(
match_on=['method', 'scheme', 'host', 'port', 'path', 'query', 'body'],
)Raw body matching can be brittle when equivalent JSON is serialized in a different key order or contains timestamps.
Add a domain-specific request matcherregister-custom-matcher
def tenant_match(r1, r2):
assert r1.headers.get('X-Tenant') == r2.headers.get('X-Tenant')
my_vcr = vcr.VCR()
my_vcr.register_matcher('tenant', tenant_match)
with my_vcr.use_cassette('tenant.yml', match_on=['method', 'uri', 'tenant']):
...A matcher signals mismatch by raising AssertionError; keep its message useful because it becomes the cassette mismatch explanation.
Use JSON instead of YAMLstore-json-cassette
with vcr.use_cassette(
'tests/cassettes/users.json',
serializer='json',
record_mode='once',
):
...Changing serializers does not convert existing cassettes; rerecord or migrate the fixture and keep its extension consistent.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| responses | PyPI | You use requests and prefer concise, explicitly authored response mocks |
| requests-mock | PyPI | You want a requests adapter or pytest fixture with programmatic matching |
| respx | PyPI | Your client is HTTPX and async-aware route mocking is more important than recording |
| pytest-httpx | PyPI | You want pytest-native HTTPX responses and strict unused-response checks |