mrkeyoor.com_
Sat 08 Aug 21:00 UTC
PyPITestingupdated 08 Aug 2026

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.

Verdict

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.

API stability4/5The use_cassette decorator/context manager, VCR configuration object, record modes, filters, serializers, and matchers have formed a stable surface for years. Major releases still drop old Python versions and adapt to changes in patched HTTP libraries, and any patch-based tool has compatibility risk when requests, urllib3, HTTPX, or asyncio internals move.
Docs4/5The Read the Docs site explains configuration, record modes, request matching, filtering, custom matchers, serializers, advanced use, asyncio, and supported libraries, while the README gives a clear mental model for cassette replay. Security guidance exists but deserves more prominence because filtering after recording cannot retroactively protect a committed cassette.
Maintenance4/5Version 8.3.0 was published on 2026-07-04 and the repository had 2,981 stars, 156 open issues and PRs, and a push on 2026-08-04. That is current maintenance for a mature test utility. The open queue is not tiny, which reflects the broad matrix of Python HTTP clients and edge cases the project patches.
Ecosystem4/5VCR.py supports common Python HTTP paths and has integrations and conventions around pytest and unittest, with YAML or JSON cassette formats and extensible matchers and serializers. It is the recognizable Python port of Ruby VCR, but transport support is necessarily selective, so newer or native-backed HTTP clients may need separate mock tools.

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
Skip it if

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'] == 42

The 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 == 1

Network 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

PackageRegistryPick it when
responsesPyPIYou use requests and prefer concise, explicitly authored response mocks
requests-mockPyPIYou want a requests adapter or pytest fixture with programmatic matching
respxPyPIYour client is HTTPX and async-aware route mocking is more important than recording
pytest-httpxPyPIYou want pytest-native HTTPX responses and strict unused-response checks