responses
responses fakes HTTP for code that calls the requests library. It works by patching one function, requests.adapters.HTTPAdapter.send, so anything your code does through requests gets served from a registry of canned Response objects instead of the network. You register with responses.get(url, json=..., status=...) or the generic responses.add(), turn it on with the @responses.activate decorator or a with responses.RequestsMock() block, and any request that does not match a registered response raises requests.exceptions.ConnectionError rather than quietly reaching the internet. On top of that there is a matchers module for asserting on the request body, query string, headers and multipart payload, a registries module for making the order of responses meaningful, callbacks for computing a reply from the incoming request, and responses.calls for inspecting exactly what your code sent.
If your code speaks requests, this is the mocking library to reach for: one decorator, no fixture server, and matchers good enough to assert what you actually sent. Just remember it only patches requests, so keep an eye on anything in your dependency tree that talks HTTP some other way.
Use it if
- Your production code uses requests and you want tests that never touch the network: the patch is at the adapter layer, so it covers Session objects, third-party SDKs built on requests, and code you cannot edit
- You want to assert on what was sent, not just stub what comes back: matchers.json_params_matcher, query_param_matcher, header_matcher and multipart_matcher fail the request with a ConnectionError when the payload is wrong, which turns a stub into a contract test
- You need to stage a sequence: registering several responses for one URL pops them in order, and registries.OrderedRegistry makes that strict so you can exercise urllib3 Retry logic against three 500s and then a 200
- You want to test error paths cheaply: passing body=Exception('boom') makes the call raise instead of returning, so timeout and connection-failure branches get covered without sleeping
- You are already in pytest or unittest and do not want a fixture server: the decorator, the context manager, and the start/stop pair all work without changing how tests are collected or run
- Your client is httpx, aiohttp or urllib3 directly. The patch target is requests.adapters.HTTPAdapter.send and nothing else, so an httpx call inside the same test goes straight out to the real internet with no warning at all. respx covers httpx, aioresponses covers aiohttp
- You want confidence that a real server would accept your request. Nothing below requests runs: no TLS handshake, no DNS, no connection pooling, no genuine urllib3 retry timing. A test suite here can be green against an API contract that no longer exists, which is precisely the failure mode that makes people distrust mocked HTTP
- You want cassettes as a supported feature. Record and replay lives in responses._recorder, is labelled BETA in the README, and the underscore in the module name is the maintainers telling you the API can move. vcrpy has done this for a decade with a public interface
- You have tight dependency pins. 0.26.2 requires requests >=2.30,<3.0 and urllib3 >=1.25.10,<3.0 and also pulls in pyyaml for the recorder, so an older locked stack can hit resolver conflicts over a test-only dependency
- Your tests run in parallel threads sharing one process. responses.mock is a module-level singleton guarded by a lock, so concurrent tests serialize on it, and a stray registration in setUp leaks into whatever runs next unless you reset carefully
- You expect the decorator and the context manager to behave the same. They do not: RequestsMock() defaults assert_all_requests_are_fired to True while @responses.activate defaults it to False, so the same unused stub fails one style of test and passes the other
Setup reality
pip install responses and the decorator works on the first try, which is why the surprises come later. First, the patch is applied to one attribute path, requests.adapters.HTTPAdapter.send, so any code that built its own transport adapter or that reaches urllib3 directly slips past unmocked; the target argument on RequestsMock exists precisely because people need to point it somewhere else. Second, the defaults disagree between the two entry points: @responses.activate leaves assert_all_requests_are_fired at False, while with responses.RequestsMock() sets it to True, so moving a test from one form to the other can change whether an unused stub is an error. Third, matching is stricter than it looks. Query strings are matched when the registered URL has one and ignored when it does not, header_matcher with strict_match=True fails against the headers requests adds on its own, and json_params_matcher compares the parsed body exactly, so an extra key that your API happily ignores still turns into a ConnectionError with a message about a mismatch rather than an obvious assertion failure. Budget an afternoon learning to read those error strings. Finally, a failed match surfaces as requests.exceptions.ConnectionError, which is the same exception your production code probably catches and retries, so a badly written test can look like a hang rather than a failure.
Patterns
Register a response and hit itstub-a-get
import responses
import requests
import pytest
@responses.activate
def test_fetches_user():
responses.get(
"https://api.example.com/users/1",
json={"id": 1, "name": "Ada"},
status=200,
)
resp = requests.get("https://api.example.com/users/1")
assert resp.json()["name"] == "Ada"
@responses.activate
def test_unregistered_url_is_blocked():
with pytest.raises(requests.exceptions.ConnectionError):
requests.get("https://api.example.com/anything-else")The json= keyword sets the Content-Type for you; body= does not, so pair body with content_type when the caller checks it. An unmatched URL raises ConnectionError, which is the same exception a real outage produces, so if your code under test has a broad except around requests you will see a retry loop instead of a failing assertion.
Catch stubs that were never calledcontext-manager-and-unused-stubs
import responses
import requests
def test_context_manager():
with responses.RequestsMock() as rsps: # asserts on exit by default
rsps.get("https://api.example.com/a", json={})
rsps.get("https://api.example.com/b", json={})
requests.get("https://api.example.com/a")
# AssertionError: Not all requests have been executed
# [('GET', 'https://api.example.com/b')]
@responses.activate(assert_all_requests_are_fired=True)
def test_decorator_opt_in():
responses.get("https://api.example.com/b", json={})
# same assertion, but only because it was asked for explicitlyThis is the one default worth memorising: RequestsMock() sets assert_all_requests_are_fired to True, while @responses.activate sets it to False. A test that quietly stops calling an endpoint keeps passing under the decorator and starts failing the moment someone converts it to a with block.
Assert on the JSON or form body that was sentmatch-request-body
import responses
import requests
from responses import matchers
@responses.activate
def test_sends_right_payload():
responses.post(
"https://api.example.com/orders",
json={"id": "ord_1"},
match=[matchers.json_params_matcher({"sku": "abc", "qty": 2})],
)
requests.post("https://api.example.com/orders", json={"sku": "abc", "qty": 2})
@responses.activate
def test_form_encoded():
responses.post(
"https://calc.example.com/sum",
body="4",
match=[matchers.urlencoded_params_matcher({"left": "1", "right": "3"})],
)
requests.post("https://calc.example.com/sum", data={"left": 1, "right": 3})Matching is exact by default, so one extra key your API would happily ignore turns into a ConnectionError whose message contains the diff. urlencoded_params_matcher compares strings, which is why the expected values are quoted even though the code passes integers. Reach for matchers.body_matcher when the payload is neither JSON nor form data.
Match on query parameters and headersmatch-query-and-headers
import responses
import requests
from responses import matchers
@responses.activate
def test_query_and_headers():
responses.get(
"https://api.example.com/search", # no query string in the URL
json={"hits": []},
match=[
matchers.query_param_matcher({"q": "ada", "page": "2"}),
matchers.header_matcher({"Accept": "application/json"}),
],
)
requests.get(
"https://api.example.com/search",
params={"q": "ada", "page": 2},
headers={"Accept": "application/json"},
)
# only check the params you care about
matchers.query_param_matcher({"q": "ada"}, strict_match=False)Do not put the query string in the URL when you also use query_param_matcher; the URL's own query is matched separately and the two rules fight. header_matcher ignores the extra headers requests adds by default, and turning on strict_match=True makes it fail against User-Agent and Accept-Encoding unless you send a hand-built PreparedRequest.
Return different answers to the same URLsequence-of-responses
import responses
import requests
from responses import registries
from urllib3.util import Retry
@responses.activate
def test_first_fails_then_succeeds():
responses.get("https://api.example.com/x", status=500)
responses.get("https://api.example.com/x", json={"ok": True})
assert requests.get("https://api.example.com/x").status_code == 500
assert requests.get("https://api.example.com/x").json() == {"ok": True}
@responses.activate(registry=registries.OrderedRegistry)
def test_retry_budget():
url = "https://api.example.com/x"
for _ in range(3):
responses.get(url, body="Error", status=500)
responses.get(url, body="OK", status=200)
session = requests.Session()
session.mount(
"https://",
requests.adapters.HTTPAdapter(
max_retries=Retry(total=4, backoff_factor=0, status_forcelist=[500])
),
)
assert session.get(url).status_code == 200With the default registry, a response is consumed only when more than one matches; register a single response for a URL and it answers every call. OrderedRegistry makes insertion order binding, which is the only reliable way to test a retry budget. Set backoff_factor to 0 or your test sleeps for real.
Compute the reply from the incoming requestdynamic-callback
import json
import re
import responses
import requests
@responses.activate
def test_echo_sum():
def callback(request):
payload = json.loads(request.body)
body = json.dumps({"value": sum(payload["numbers"])})
return (200, {"request-id": "abc123"}, body)
responses.add_callback(
responses.POST,
re.compile(r"https://calc\.example\.com/(sum|prod)"),
callback=callback,
content_type="application/json",
)
resp = requests.post("https://calc.example.com/sum", json={"numbers": [1, 2, 3]})
assert resp.json() == {"value": 6}The callback must return a three-tuple of status, headers and a body string; returning a dict or forgetting the headers raises inside responses rather than in your test. request.body is bytes or str depending on how the caller sent it, so parse defensively. Use functools.partial to reuse one callback with different canned identifiers.
Assert how many times and with whatinspect-calls
import json
import responses
import requests
@responses.activate
def test_call_inspection():
rsp = responses.patch("https://api.example.com/users/1", status=200)
requests.patch("https://api.example.com/users/1", json={"active": False})
assert len(responses.calls) == 1 # every call in this test
assert rsp.call_count == 1 # calls matched by this stub
assert json.loads(rsp.calls[0].request.body) == {"active": False}
assert rsp.calls[0].response.status_code == 200
responses.assert_call_count("https://api.example.com/users/1", 1)Prefer rsp.calls over indexing responses.calls when requests can arrive in any order, for example from a thread pool, because the per-response list is what stays stable. assert_call_count matches on the exact URL including its query string, so a call with extra params counts as a different URL and the assertion fails in a confusing way.
Make the call blow up instead of returningsimulate-network-errors
import pytest
import requests
import responses
@responses.activate
def test_timeout_path():
responses.get(
"https://api.example.com/slow",
body=requests.exceptions.ConnectTimeout("too slow"),
)
with pytest.raises(requests.exceptions.ConnectTimeout):
requests.get("https://api.example.com/slow", timeout=1)
@responses.activate
def test_body_can_be_any_exception():
responses.get("https://api.example.com/boom", body=Exception("kaboom"))
with pytest.raises(Exception, match="kaboom"):
requests.get("https://api.example.com/boom")Passing an exception instance as body is how you cover retry and fallback branches without waiting on a real timeout. Raise the specific exception your code catches: a bare Exception will slip past an except requests.exceptions.RequestException and fail the test for the wrong reason.
Let some hosts through to the networkallow-real-requests
import re
import responses
@responses.activate
def test_mixed():
responses.add_passthru("https://s3.amazonaws.com") # prefix
responses.add_passthru(re.compile(r"https://cdn\.\w+\.com/")) # pattern
responses.get("https://api.example.com/x", json={"ok": True})
# calls to api.example.com are stubbed; the two passthru targets are real
# or mark one registered response as a passthrough
responses.add(
responses.Response(responses.GET, "https://api.example.com/health", passthrough=True)
)Passthru is the escape hatch for a service you genuinely cannot fake, such as a signed upload, but it also puts the network back into your test suite: CI now fails when that host is down. Keep the prefixes narrow and never pass a bare scheme, since a prefix of 'https://' disables the whole point of the library.
Start and stop around a test class or fixtureshare-across-tests
import pytest
import responses
@pytest.fixture
def mocked_api():
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
rsps.get("https://api.example.com/health", json={"status": "ok"})
yield rsps
def test_uses_fixture(mocked_api):
mocked_api.get("https://api.example.com/users", json=[])
...
class TestManualLifecycle:
def setup_method(self):
self.r_mock = responses.RequestsMock(assert_all_requests_are_fired=True)
self.r_mock.start()
self.r_mock.get("https://api.example.com/health", status=200)
def teardown_method(self):
self.r_mock.stop()
self.r_mock.reset()A yielding fixture is the safest shared setup because the context manager unpatches even when the test raises. If you use start/stop by hand, put stop() and reset() in teardown unconditionally: a missed stop leaves HTTPAdapter.send patched for the rest of the session and the failures land in unrelated tests.
Replace, upsert and remove stubs mid-testedit-registered-responses
import responses
import requests
@responses.activate
def test_swap_response():
responses.get("https://api.example.com/cfg", json={"flag": False})
assert requests.get("https://api.example.com/cfg").json() == {"flag": False}
responses.replace(responses.GET, "https://api.example.com/cfg", json={"flag": True})
assert requests.get("https://api.example.com/cfg").json() == {"flag": True}
print(responses.registered()) # inspect the stack while debugging
responses.remove(responses.GET, "https://api.example.com/cfg")
responses.reset()replace() only touches the first stub matching that method and URL and raises if none exists; upsert() adds one instead when it is missing. remove() deletes every match, not just the first, which is the asymmetry that catches people. responses.registered() is the fastest way to see why a match failed.
Capture real traffic to a YAML filerecord-and-replay
import requests
import responses
from responses import _recorder
@_recorder.record(file_path="tests/fixtures/api.yaml")
def capture():
requests.get("https://api.example.com/users/1")
requests.get("https://api.example.com/users/2")
@responses.activate
def test_replays_recording():
responses._add_from_file(file_path="tests/fixtures/api.yaml")
responses.post("https://api.example.com/users", status=201) # add extras after
assert requests.get("https://api.example.com/users/1").status_code == 200Both entry points start with an underscore and the README labels this BETA, so pin your version if you build a workflow on it. The recorder writes status, body, headers and URL verbatim, which means any API token or personal data in a captured response lands in a file you are about to commit. Scrub before you push, or use vcrpy, which has filtering built in.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| respx | PyPI | Your client is httpx, sync or async, where responses patches nothing and every call escapes to the network |
| requests-mock | PyPI | You prefer adapter-level mounting and a fixture-first API, or you want to mock a single Session rather than all of requests |
| vcrpy | PyPI | You want recorded cassettes as a first-class, stable feature instead of the BETA recorder hidden behind responses._recorder |
| pytest-httpserver | PyPI | You want a real local HTTP server so the socket, TLS and client library behaviour are exercised rather than patched away |