VCR.py: Make Public API Tests Repeatable in Python VCR.py records HTTP requests and responses during a test and replays them on later runs, letting Python tests reuse recorded API responses instead of calling a live API each time. The tutorial demonstrates the tool with vcrpy v8.3.0, requests v2.32.4, and pytest v9.1.1 against a client that reads repository metadata from the GitHub REST API, saving each request and response to a cassette file. The approach removes network, API server, and rate-limit dependencies that can cause tests to fail even when the code under test is correct. Table of Contents Introduction A good test keeps the moving parts small. If the inputs, dependencies, and environment stay the same, a failure is more likely to point to your code. That is hard when your code depends on a public API. A public API adds things you do not control, such as the network, the API server, and rate limits. That means the test can fail even when your code did nothing wrong. VCR.py helps by letting tests reuse recorded API responses instead of calling the live API every time. This article shows how to use VCR.py to make API-dependent tests more repeatable. 💻 Get the Code: The reproducible examples are in notebooks/record-replay-api-tests-vcrpy ../notebooks/record-replay-api-tests-vcrpy/ , including the GitHub API client, pytest tests, and recorded cassette. Stay Current with CodeCut Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week. .codecut-subscribe-wrap { display: flex; justify-content: center; } .codecut-subscribe-btn { background: 72BEFA important; color: 2F2D2E important; border: none; border-radius: 8px; padding: 12px 28px; font-family: inherit; font-size: 16px; font-weight: 700; cursor: pointer; text-decoration: none important; display: inline-flex; align-items: center; justify-content: center; transition: background 0.3s ease; } .codecut-subscribe-btn:hover, .codecut-subscribe-btn:focus { background: 5aa8e8 important; color: 2F2D2E important; text-decoration: none important; } / Mobile responsive / @media max-width: 480px { .codecut-subscribe-btn { width: 100%; text-align: center; } } What Is VCR.py? VCR.py https://github.com/kevin1024/vcrpy records HTTP requests and responses during a test, then replays them in later runs so the test no longer depends on the live API every time. The workflow is simple: Run the test once and let it make the real HTTP request. VCR.py saves the request and response to a cassette file. Later test runs replay the saved response instead of calling the network. The next sections show how this works with a practical pytest example. Setup Install the libraries used in this tutorial: pip install vcrpy requests pytest This article uses vcrpy v8.3.0, requests v2.32.4, and pytest v9.1.1. We will test a small client that reads repository metadata from the GitHub REST API https://docs.github.com/en/rest/repos/repos . Create a file named repo client.py that does the following: Makes an external HTTP request. Parses nested JSON. Depends on fields controlled by another service. import requests def get repository summary owner: str, repo: str - dict str, str | int | None : Call the live GitHub API. url = f"https://api.github.com/repos/{owner}/{repo}" response = requests.get url, timeout=10 response.raise for status Parse the JSON fields the app needs. data = response.json license data = data.get "license" Return a smaller, app-specific summary. return { "full name": data "full name" , "description": data "description" , "stars": data "stargazers count" , "license": license data "spdx id" if license data else None, "default branch": data "default branch" , } For this example, the test should answer two questions: Did the request reach the right repository endpoint? Did the function extract the fields correctly from the JSON response? Write the Live API Test First, write the test without VCR.py. It calls the live GitHub API and verifies that the client returns the expected repository summary. from repo client import get repository summary def test get repository summary live : summary = get repository summary "kevin1024", "vcrpy" assert summary "full name" == "kevin1024/vcrpy" assert summary "license" == "MIT" assert summary "default branch" == "master" While the test passes, the future test runs can fail because: GitHub is unavailable. Your machine has no network access. The request takes too long. The API returns a temporary error. A failed test becomes hard to interpret: did the client code break, or did the API fail to respond? At that point, the test is no longer only testing the client code. VCR.py addresses this by turning the API response into a local fixture. The test can keep checking the client code without depending on whether GitHub responds. Record the API Response Once First, import VCR.py and the function you want to test: import vcr from repo client import get repository summary Next, create a VCR configuration for this test file: Store cassettes in a predictable test folder. github vcr = vcr.VCR cassette library dir="tests/fixtures/cassettes", record mode="once", With once, VCR.py records only when it needs to create the cassette: If the cassette does not exist, VCR.py calls the API and records the response. If the cassette exists, VCR.py replays the matching response from the cassette. Next, use use cassette to tell VCR.py which cassette file this test should record to and replay from: Record this request once, then replay it later. @github vcr.use cassette "github vcrpy.yaml" def test get repository summary with vcr : summary = get repository summary "kevin1024", "vcrpy" Run it the same way you run any pytest test: pytest tests/test repo client.py On the first run, VCR.py does not have a cassette yet. It lets the request go to GitHub, captures the response, and writes it to tests/fixtures/cassettes/github vcrpy.yaml. The cassette contains the recorded HTTP interaction. A shortened version looks like this: interactions: - request: method: GET uri: https://api.github.com/repos/kevin1024/vcrpy response: status: code: 200 message: OK body: string: '{"id": 3736670, "name": "vcrpy", ...}' version: 1 See the full cassette on GitHub https://github.com/khuyentran1401/codecut-blog/blob/main/record-replay-api-tests-vcrpy/tests/fixtures/cassettes/github vcrpy.yaml for the complete recorded request and response. Once the cassette exists, VCR.py can use it on future test runs. Replay the Test Offline Run the same test again: pytest tests/test repo client.py This time, VCR.py sees the cassette file. Instead of sending another request to GitHub, it replays the saved response. This gives you a more stable API test: The client still parses a real GitHub response, so the test uses real response data. The test no longer needs GitHub on every run, so temporary external issues do not break it. If you refresh the cassette, Git shows the recorded response changes for review. Control When Cassettes Change By default, VCR.py can record a cassette when one is missing. That is useful locally, where you can inspect the new file before committing it. In a GitHub Actions workflow https://codecut.ai/run-github-actions-locally-act/ , that is risky because the build can create or update a fixture without review, making a passing test harder to trust. To prevent CI from recording new API responses, set record mode="none" in the CI environment: import vcr ci vcr = vcr.VCR cassette library dir="tests/fixtures/cassettes", record mode="none", With none, VCR.py only replays existing cassettes: If the cassette exists, VCR.py replays matching requests from the cassette. If the cassette is missing, the test fails. If your code makes a new unmatched request, the test fails. Use the VCR instance above in your test: from repo client import get repository summary @ci vcr.use cassette "github vcrpy.yaml" def test get repository summary in ci : summary = get repository summary "kevin1024", "vcrpy" With this configuration, CI can only use responses that were already recorded and committed. If the API response needs to change, you can re-record the cassette locally, inspect the Git diff, and commit that update intentionally. VCR.py vs Mocking For those who are familiar with mocking, you might be wondering: “Why should I use VCR.py instead of mocking?” The key difference is the source of the response: mocking replaces the API with a fake response, while VCR.py replays a response recorded from the real API. The code below shows a test that uses mocking to control the API response: from unittest.mock import Mock, patch def get repository license owner: str, repo: str - str | None: response = requests.get f"https://api.github.com/repos/{owner}/{repo}" response.raise for status data = response.json return data "license" "spdx id" if data "license" else None @patch "requests.get" def test get repository license with mock mock get : mock response = Mock mock response.json.return value = {"license": {"spdx id": "MIT"}} mock response.raise for status.return value = None mock get.return value = mock response license id = get repository license "kevin1024", "vcrpy" assert license id == "MIT" In this test: @patch "requests.get" replaces the real HTTP call during the test. mock response acts like the response object requests.get would normally return. mock response.json.return value defines the JSON payload used in the test. The assertion checks that the test-provided payload leads to "MIT". This graph shows the difference between a mock and VCR.py: So why should you use VCR.py instead of mocking? Mocks https://codecut.ai/pytest-for-data-scientists/ are useful when the goal is to isolate your code. For example, this mock forces a successful response: mock response.json.return value = {"license": {"spdx id": "MIT"}} mock response.raise for status.return value = None That is a good unit test, but it does not check the real HTTP interaction. A public API can also return status codes and headers that affect your client: Status: 403 X-RateLimit-Remaining: 0 For that kind of integration-style test, VCR.py is a better fit because it records the real HTTP response once and replays it later. A good testing stack often uses all three layers: Layer Purpose Mocked unit tests Fast checks for your own logic VCR.py tests Deterministic tests against recorded real HTTP responses A few live smoke tests Confirmation that the external service still behaves as expected A Practical VCR.py Testing Workflow Some good practices when using VCR.py: Commit cassettes with the tests that use them, so every test has the response fixture it needs. Run CI in replay-only mode, so builds cannot create or update fixtures without review. Review cassette diffs before committing them, so response changes do not get added to your tests unnoticed. That keeps API tests stable without hiding external changes. 📚 For more context on building a complete testing and CI workflow, see Production-Ready Data Science https://codecut.ai/production-ready-data-science/ . References VCR.py API docs https://vcrpy.readthedocs.io/en/latest/api.html VCR.py docs, 2026 : Configuration options including record mode, match on, serializer, and cassette library dir. VCR.py usage docs https://vcrpy.readthedocs.io/en/latest/usage.html VCR.py docs, 2026 : Record modes including once, new episodes, none, and all. The post VCR.py: Make Public API Tests Repeatable in Python https://codecut.ai/vcrpy-repeatable-public-api-tests-python/ appeared first on CodeCut https://codecut.ai .