Claude Code for VCR.py: Record and Replay HTTP in Tests — Claude Skills 360 Blog
Blog / AI / Claude Code for VCR.py: Record and Replay HTTP in Tests
AI

Claude Code for VCR.py: Record and Replay HTTP in Tests

Published: January 11, 2028
Read time: 5 min read
By: Claude Skills 360

VCR.py records real HTTP calls and replays them in tests. pip install vcrpy. Decorator: import vcr. @vcr.use_cassette("cassettes/users.yaml") def test_list_users(): resp = requests.get("https://api.example.com/users"); assert resp.status_code == 200. Context: with vcr.use_cassette("cassettes/create.yaml"): resp = requests.post(url, json=data). First run: makes real HTTP call and saves to YAML file. Subsequent runs: replays from file — no network. Record modes: record_mode="none" — fail if cassette missing. record_mode="new_episodes" — replay if exists, record new URIs. record_mode="all" — always re-record. record_mode="once" (default) — record if missing, replay if present. Config: my_vcr = vcr.VCR(cassette_library_dir="tests/cassettes", record_mode="none"). Match on: match_on=["method","scheme","host","port","path","query"] — tune what counts as the same request. match_on=["uri","method","body"]. Filter headers: my_vcr = vcr.VCR(filter_headers=["Authorization","X-API-Key"]) — strip secrets before saving. filter_post_data_parameters=["password","token"]. Hook: before_record_request=lambda req: req — transform before recording. before_record_response=lambda resp: resp. JSON cassette: serializer="json". decode_compressed_response=True. Python Requests: supported out of the box. httpx: pip install pytest-httpx or use vcr-httpx extra. aiohttp: pip install vcrpy[aiohttp]. urllib: supported. pytest: pip install pytest-recording. @pytest.mark.vcr — auto-discovers cassette at cassettes/test_module/test_name.yaml. pytest --vcr-record=none. CI: commit cassettes to source control — tests run without network. re_record_interval — auto-expire cassettes. Claude Code generates VCR.py cassette fixtures, record-mode configurations, and secret-scrubbing hooks.

CLAUDE.md for VCR.py

## VCR.py Stack
- Version: vcrpy >= 6.0 | pip install vcrpy pytest-recording
- Record: @vcr.use_cassette("path.yaml") | with vcr.use_cassette(...)
- Mode: record_mode="none" (CI) | "once" (default) | "new_episodes" | "all"
- Secrets: filter_headers=["Authorization"] | filter_post_data_parameters=["token"]
- Match: match_on=["uri","method","body"] — tune request identity
- pytest: @pytest.mark.vcr — auto-cassette path from test name
- Dir: cassette_library_dir="tests/cassettes" in VCR() config object

VCR.py Record-Replay Pipeline

# tests/test_with_vcr.py — VCR.py cassette usage
from __future__ import annotations

import json

import pytest
import requests
import vcr


# ─────────────────────────────────────────────────────────────────────────────
# VCR instance — project-wide settings
# ─────────────────────────────────────────────────────────────────────────────

PROJECT_VCR = vcr.VCR(
    cassette_library_dir="tests/cassettes",
    record_mode="none",             # fail if cassette missing — safe for CI
    match_on=["uri", "method", "body"],
    filter_headers=["Authorization", "X-API-Key", "Cookie"],
    filter_post_data_parameters=["password", "token", "secret"],
    decode_compressed_response=True,
    serializer="yaml",              # human-readable cassette files
)


# ─────────────────────────────────────────────────────────────────────────────
# Client under test
# ─────────────────────────────────────────────────────────────────────────────

BASE_URL = "https://jsonplaceholder.typicode.com"


class PostApiClient:
    """Thin wrapper around the JSONPlaceholder demo API."""

    def list_posts(self, user_id: int | None = None) -> list[dict]:
        params = {"userId": user_id} if user_id else {}
        r = requests.get(f"{BASE_URL}/posts", params=params,
                         headers={"Authorization": "Bearer secret-token"})
        r.raise_for_status()
        return r.json()

    def get_post(self, post_id: int) -> dict:
        r = requests.get(f"{BASE_URL}/posts/{post_id}")
        r.raise_for_status()
        return r.json()

    def create_post(self, payload: dict) -> dict:
        r = requests.post(f"{BASE_URL}/posts", json=payload)
        r.raise_for_status()
        return r.json()

    def update_post(self, post_id: int, payload: dict) -> dict:
        r = requests.patch(f"{BASE_URL}/posts/{post_id}", json=payload)
        r.raise_for_status()
        return r.json()

    def delete_post(self, post_id: int) -> None:
        r = requests.delete(f"{BASE_URL}/posts/{post_id}")
        r.raise_for_status()


@pytest.fixture
def api() -> PostApiClient:
    return PostApiClient()


# ─────────────────────────────────────────────────────────────────────────────
# 1. @vcr.use_cassette decorator — one cassette per test
# ─────────────────────────────────────────────────────────────────────────────

class TestListPosts:

    @vcr.use_cassette("tests/cassettes/list_posts.yaml")
    def test_returns_list(self, api: PostApiClient) -> None:
        """
        First run: makes real GET request, saves cassette.
        Subsequent runs: replays from cassette — no network needed.
        """
        posts = api.list_posts()
        assert isinstance(posts, list)
        assert len(posts) > 0
        assert "title" in posts[0]

    @vcr.use_cassette("tests/cassettes/list_posts_by_user.yaml")
    def test_filter_by_user(self, api: PostApiClient) -> None:
        posts = api.list_posts(user_id=1)
        assert all(p["userId"] == 1 for p in posts)

    @PROJECT_VCR.use_cassette("get_post_1.yaml")
    def test_get_single_post(self, api: PostApiClient) -> None:
        """Uses the project-wide VCR instance — cassette at tests/cassettes/get_post_1.yaml."""
        post = api.get_post(1)
        assert post["id"] == 1
        assert "title" in post


# ─────────────────────────────────────────────────────────────────────────────
# 2. Context manager — inline mock within test body
# ─────────────────────────────────────────────────────────────────────────────

class TestCreatePost:

    def test_create_returns_201(self, api: PostApiClient) -> None:
        payload = {"title": "Test", "body": "Hello", "userId": 1}
        with vcr.use_cassette("tests/cassettes/create_post.yaml"):
            result = api.create_post(payload)
        assert "id" in result

    def test_update_post(self, api: PostApiClient) -> None:
        with PROJECT_VCR.use_cassette("update_post_1.yaml"):
            result = api.update_post(1, {"title": "Updated title"})
        assert "id" in result

    def test_delete_post(self, api: PostApiClient) -> None:
        with PROJECT_VCR.use_cassette("delete_post_1.yaml"):
            api.delete_post(1)   # no exception = success


# ─────────────────────────────────────────────────────────────────────────────
# 3. pytest-recording — @pytest.mark.vcr auto-cassette
# ─────────────────────────────────────────────────────────────────────────────

class TestWithPytestRecording:
    """
    pytest-recording (pip install pytest-recording) provides @pytest.mark.vcr.
    Cassette path is derived automatically: tests/cassettes/<module>/<TestClass.method>.yaml
    Run with: pytest --vcr-record=none   (CI: never hit network)
              pytest --vcr-record=once   (local: record if missing)
    """

    @pytest.mark.vcr
    def test_list_posts_auto_cassette(self, api: PostApiClient) -> None:
        posts = api.list_posts()
        assert len(posts) > 0

    @pytest.mark.vcr
    def test_get_post_auto_cassette(self, api: PostApiClient) -> None:
        post = api.get_post(1)
        assert post["id"] == 1

    @pytest.mark.vcr(record_mode="new_episodes")
    def test_list_with_new_episodes(self, api: PostApiClient) -> None:
        """Override record mode per-test."""
        posts = api.list_posts(user_id=2)
        assert len(posts) > 0


# ─────────────────────────────────────────────────────────────────────────────
# 4. Secret scrubbing — verify Authorization header is not saved
# ─────────────────────────────────────────────────────────────────────────────

class TestSecretScrubbing:

    @PROJECT_VCR.use_cassette("scrubbed_request.yaml")
    def test_auth_header_not_in_cassette(self, api: PostApiClient) -> None:
        """
        PROJECT_VCR strips Authorization header before writing cassette.
        The cassette file will NOT contain "Bearer secret-token".
        """
        posts = api.list_posts()
        assert posts is not None
        # Manually verify cassette is clean:
        import os
        cassette_path = "tests/cassettes/scrubbed_request.yaml"
        if os.path.exists(cassette_path):
            with open(cassette_path) as f:
                content = f.read()
            assert "secret-token" not in content


# ─────────────────────────────────────────────────────────────────────────────
# 5. before_record_request hook — transform requests before saving
# ─────────────────────────────────────────────────────────────────────────────

def _scrub_request(request: vcr.request.Request) -> vcr.request.Request:
    """Remove credentials and add a stable request ID for matching."""
    request.headers.pop("Authorization", None)
    request.headers.pop("X-Request-Id", None)
    return request


def _scrub_response(response: dict) -> dict:
    """Remove volatile response headers before saving cassette."""
    volatile = {"Date", "X-Request-Id", "CF-Ray", "X-RateLimit-Remaining"}
    for header in volatile:
        response["headers"].pop(header, None)
    return response


CLEAN_VCR = vcr.VCR(
    cassette_library_dir="tests/cassettes",
    record_mode="none",
    before_record_request=_scrub_request,
    before_record_response=_scrub_response,
)


class TestCustomHooks:

    @CLEAN_VCR.use_cassette("hooked_request.yaml")
    def test_list_posts_with_hooks(self, api: PostApiClient) -> None:
        posts = api.list_posts()
        assert isinstance(posts, list)


# ─────────────────────────────────────────────────────────────────────────────
# 6. conftest.py — shared VCR config (copy to tests/conftest.py)
# ─────────────────────────────────────────────────────────────────────────────

# tests/conftest.py — enable pytest-recording with project defaults:
#
# import pytest
#
# @pytest.fixture(scope="module")
# def vcr_config():
#     return {
#         "filter_headers": ["Authorization", "Cookie", "X-API-Key"],
#         "filter_post_data_parameters": ["password", "token"],
#         "record_mode": "none",
#         "match_on": ["uri", "method", "body"],
#         "decode_compressed_response": True,
#     }
#
# @pytest.fixture(scope="module")
# def vcr_cassette_dir(request):
#     # Group cassettes by test module
#     return f"tests/cassettes/{request.module.__name__}"

For the responses / respx mocking alternative — responses and respx require you to manually encode every API response as a dict in the test file, so when the real API changes its response shape your tests silently continue passing with the stale hand-crafted mock, while VCR.py records the actual server response on first run — the cassette YAML contains the real headers, status code, and body — and any schema change will break the next record_mode="once" run, immediately surfacing the drift. For the pytest fixtures with mocked responses alternative — per-test mock fixtures must be updated whenever the external service changes, while VCR.py cassettes can be refreshed on demand with record_mode="all" or re_record_interval="7 days" — run the suite in record mode weekly to keep cassettes fresh, then switch to record_mode="none" for CI so tests never hit the network and cassette drift is caught at refresh time. The Claude Skills 360 bundle includes VCR.py skill sets covering vcr.use_cassette decorator and context manager, record_mode none/once/new_episodes/all, filter_headers and filter_post_data_parameters for secret scrubbing, before_record_request and before_record_response transformation hooks, match_on configuration for request identity, pytest-recording @pytest.mark.vcr integration, conftest.py vcr_config and vcr_cassette_dir fixtures, cassette directory organization, and decode_compressed_response for gzip APIs. Start with the free tier to try HTTP record-replay testing code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free