responses review
responses 0.26.2 replaces outbound calls made through Python requests with registered in-memory results. A test activates the patch, associates a method and URL with JSON, bytes, an exception, or a callback, and can inspect every matched call afterward. Built-in matchers cover query parameters, JSON, forms, headers, multipart uploads, URL fragments, and selected request arguments. Unknown requests raise requests.exceptions.ConnectionError unless passthrough is enabled. Version 0.26.2 fixes numeric query matching that changed the caller's dictionary and removes recorded default headers regardless of their letter case, including lowercase HTTP/2-style names.
responses 0.26.2 installed in 0.3 seconds, used 6 MB across 7 packages, and imported in 0.59 seconds with 0 audit findings in our sandbox. Use it for precise requests unit tests; use a real test server when the network stack itself is part of the claim.
We installed it
| Install | ✓ · 0.3s | 7 packages on disk · 6 MB |
| Import | ✓ | import responses in 0.59s · pure Python · py.typed · requires Python >=3.8 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does responses install cleanly?
Yes. In a fresh container with an empty cache, pip install responses finished in 0.3s, leaving 7 packages and 6 MB on disk. pip-audit reported no known vulnerabilities.
What does responses need to run?
Python >=3.8, and nothing compiled: it is pure Python. In our run import responses succeeded in 0.59s, and the package ships py.typed for type checkers.
responses or requests-mock: which should you use?
requests-mock: Choose it for adapter mounting, a pytest fixture, or mocking one requests Session rather than process-wide activation. responses 0.26.2 installed in 0.3 seconds, used 6 MB across 7 packages, and imported in 0.59 seconds with 0 audit findings in our sandbox.
When should you not use responses?
The client is HTTPX or aiohttp. responses patches requests.adapters.HTTPAdapter.send and will not intercept those transports.
Use it if
- Application or SDK code sends HTTP through requests or a normal requests.Session adapter.
- Unit tests must assert the exact query, JSON, form, header, multipart, or request-option contract.
- A retry path needs a deterministic series of failures and successes with no remote server.
- Tests need decorator, context-manager, fixture, callback, and call-history options around one patched transport.
- The client is HTTPX or aiohttp. responses patches requests.adapters.HTTPAdapter.send and will not intercept those transports.
- The behavior under test includes DNS, TLS, sockets, proxies, connection pooling, or server protocol handling. Use a local HTTP server.
- Recorded fixtures are the core workflow and need a stable public API. The recorder entry points remain underscored and documented as beta.
- Tests run simultaneous thread-level activation against shared global state. Scope a mock per test or use an isolated server to avoid leaked registrations.
- A passthrough policy would be broad enough to allow arbitrary internet access. That defeats the default failure on unregistered URLs.
Setup reality
Our responses 0.26.2 install completed in 0.3 seconds in a fresh Python 3.12 Bookworm container. It left 7 packages using 6 MB, and import responses took 0.59 seconds. pip-audit found 0 known vulnerabilities. The pure-Python package declares 14 direct dependencies, requires Python >=3.8, includes py.typed, and uses Apache-2.0.
Activation replaces requests.adapters.HTTPAdapter.send. A custom adapter or a call through urllib3, HTTPX, or aiohttp can still reach the network. Keep an independent network-deny rule in CI if accidental traffic is unacceptable. Passthrough prefixes deliberately restore real I/O, so restrict them to a precise host or route rather than https://.
Use a decorator, RequestsMock context, or yielding fixture and make the unused-registration policy explicit with assert_all_requests_are_fired. Manual start() requires unconditional stop() and reset() in teardown. Otherwise the next test can inherit both the patched adapter and old registrations, producing order-dependent failures.
Matchers compare the request that requests actually prepares. Strict headers include defaults inserted by the client; query expectations should live in either the URL or a matcher, not both. A mismatch surfaces as ConnectionError, which retry code may catch and repeat. Check call history and registered routes before blaming networking. Recorder YAML can contain authorization headers, tokens, URLs, and personal data, so scrub it before committing.
Patterns
Return JSON from one GET stub-json-get
import requests
import responses
@responses.activate
def test_user():
responses.get('https://api.example.com/users/7', json={'id': 7}, status=200)
reply = requests.get('https://api.example.com/users/7')
assert reply.json() == {'id': 7}The json argument also sets the response content type. An unregistered URL raises ConnectionError.
Require an exact JSON request match-json-body
from responses import matchers
responses.post(
'https://api.example.com/orders',
json={'id': 'ord_1'},
match=[matchers.json_params_matcher({'sku': 'abc', 'qty': 2})],
)The matcher compares parsed JSON structure. A body mismatch appears as an unmatched request rather than a custom assertion.
Match query parameters match-query
responses.get(
'https://api.example.com/search',
json={'hits': []},
match=[matchers.query_param_matcher({'q': 'ada', 'page': '2'})],
)
requests.get('https://api.example.com/search', params={'q': 'ada', 'page': 2})Keep the query out of the registered URL when using this matcher. Version 0.26.2 fixes mutation of numeric input values.
Check selected request headers match-headers
responses.get(
'https://api.example.com/data',
json={},
match=[matchers.header_matcher({'Accept': 'application/json'})],
)Non-strict matching allows default headers that requests adds. Strict mode requires accounting for the fully prepared request.
Return a failure before success sequence-replies
responses.get('https://api.example.com/job', status=503)
responses.get('https://api.example.com/job', json={'ready': True}, status=200)
assert requests.get('https://api.example.com/job').status_code == 503
assert requests.get('https://api.example.com/job').json()['ready'] is TrueMultiple matching registrations are consumed in order. Disable retry sleep when testing production retry loops.
Simulate a requests timeout raise-timeout
responses.get(
'https://api.example.com/slow',
body=requests.exceptions.ConnectTimeout('timed out'),
)
with pytest.raises(requests.exceptions.ConnectTimeout):
requests.get('https://api.example.com/slow', timeout=1)Raise the same requests exception class that production code handles; a generic Exception exercises a different branch.
Calculate a response from the request dynamic-callback
def callback(request):
payload = json.loads(request.body)
return 200, {'Content-Type': 'application/json'}, json.dumps({'sum': sum(payload['values'])})
responses.add_callback(responses.POST, 'https://calc.example.com/sum', callback=callback)Callbacks return status, headers, and body. Decode request.body according to its real type and content type.
Assert the matched request history inspect-history
route = responses.patch('https://api.example.com/users/7', status=204)
requests.patch('https://api.example.com/users/7', json={'active': False})
assert route.call_count == 1
assert json.loads(route.calls[0].request.body) == {'active': False}Per-route history stays readable when several unrelated calls occur in the same activated test.
Clean up through a pytest fixture scope-fixture
@pytest.fixture
def api_mock():
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
rsps.get('https://api.example.com/health', json={'ok': True})
yield rspsThe context unpatches requests even when the test raises. Manual start() needs stop() and reset() in teardown.
Permit one real host allow-passthrough
responses.add_passthru('https://storage.example.com/public/')
responses.get('https://api.example.com/data', json={})Passthrough makes real network calls. A broad prefix such as `https://` removes the protection against accidental traffic.
Change a registered result mid-test replace-response
responses.get('https://api.example.com/config', json={'flag': False})
responses.replace(responses.GET, 'https://api.example.com/config', json={'flag': True})
assert requests.get('https://api.example.com/config').json()['flag'] is Truereplace requires an existing route. Use upsert when the route may not have been registered yet.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| requests-mock | PyPI | Choose it for adapter mounting, a pytest fixture, or mocking one requests Session rather than process-wide activation. |
| pytest-httpserver | PyPI | Choose it when a real local server should exercise the client's socket and HTTP behavior. |
| httpx | PyPI | Choose HTTPX's MockTransport when production code already uses HTTPX and should keep one client-specific test stack. |
More testing guides
pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

