Python teams often need real email delivery in integration tests without giving a test worker access to a personal mailbox. The official Relay Python package provides a small client for creating inboxes, listing messages, reading content, and extracting OTPs or authentication links.
Fit Relay into pytest fixtures

import os
from relaymcp import RelayClient
relay = RelayClient(api_key=os.environ["RELAY_API_KEY"])
inbox = relay.inboxes.create()
register_user(email=inbox.address)
message = relay.messages.wait(inbox.id, timeout=30)
assert relay.messages.otp(message.id) == expected_code
A fixture should track the inbox it creates. Create an inbox for the test or worker, expose only its address to the application under test, and preserve the identifiers needed for diagnostics. Review retention separately from local test teardown. In a parallel suite, use one inbox per worker so two tests cannot consume the same verification message. Keep the client object scoped to the fixture, close any network resources according to the package guidance, and make local teardown best effort so an assertion failure remains visible. For local debugging, keep copied diagnostic evidence only as long as the investigation needs it. Relay inboxes have no fixed expiry; consult the privacy policy and support for lifecycle and data-handling guidance. Do not place message bodies in pytest failure output by default.
| Python scenario | Recommended pattern |
|---|---|
| Unit test | Mock the mail boundary |
| Integration test | Relay inbox per worker |
| End-to-end test | Relay inbox per scenario |
| Manual investigation | Protect inbox access and minimize copied evidence |
The Python client uses the same authorization and resource model as the REST API. That means teams can start with a quick Python test and later move the workflow into a service or webhook without changing the core inbox model.
Make polling predictable
Set a deadline, use a small interval, and match the expected sender or subject before reading the message. A timeout should report the inbox ID and elapsed time, not a secret or complete message body. If the code is ambiguous, fail closed instead of choosing the first number found.
Frequently asked questions
Does it require a framework?
No. It works with pytest, unittest, scripts, and ordinary Python services.
Can it read HTML email?
Yes, but treat HTML as untrusted input and prefer extracted text for assertions.
Need another language?
The Node SDK and CLI cover JavaScript and terminal workflows.
pytest-xdist layout
| Worker | Fixture rule |
|---|---|
| gw0 | inbox = relay.inboxes.create() in function scope |
| gw1 | Never share inbox.address via module cache |
| Controller | Store API key once in pytest_configure |
Link failures to OTP testing best practices when ambiguity is the root cause.