All postsTutorials
How to Test Email in Python with pytest
The Devmailr Team7 min read
Whether you’re testing a Django signup, a FastAPI password reset, or any flow that sends mail, your pytest suite needs a way to read what your app sent. Here’s how to test email in Python with a disposable inbox — a small client, a fixture, and assertions on the code or link.
A tiny client
devmailr.py
import os, requests
BASE = "https://api.devmailr.app/api"
AUTH = {"Authorization": f"Bearer {os.environ['DEVMAILR_API_KEY']}"}
def create_inbox(prefix):
return requests.post(f"{BASE}/mailboxes", headers=AUTH, json={"prefix": prefix}).json()
def wait_for_email(mailbox_id, **params):
params.setdefault("timeout", 60)
# long-poll: give requests a read timeout above the server wait
r = requests.get(f"{BASE}/mailboxes/{mailbox_id}/wait", headers=AUTH,
params=params, timeout=params["timeout"] + 5)
if r.status_code == 404:
raise AssertionError("No matching email arrived in time")
r.raise_for_status()
return r.json()
def delete_inbox(mailbox_id):
requests.delete(f"{BASE}/mailboxes/{mailbox_id}", headers=AUTH)A pytest fixture
A fixture gives each test a fresh inbox and cleans it up afterward:
conftest.py
import time, pytest
from devmailr import create_inbox, delete_inbox
@pytest.fixture
def inbox():
box = create_inbox(f"pt-{int(time.time() * 1000)}")
yield box
delete_inbox(box["_id"])The test
test_signup.py
from devmailr import wait_for_email
def test_signup_sends_verification_code(inbox, client):
# 1. Trigger your app to send a verification email
client.post("/signup", json={"email": inbox["address"], "password": "s3cret-pw"})
# 2. Block until it arrives and pull the code
email = wait_for_email(inbox["_id"], has_code="true")
code = email["extractedCodes"][0]
assert code.isdigit()
# 3. Finish the flow
resp = client.post("/verify", json={"email": inbox["address"], "code": code})
assert resp.status_code == 200(client here is your app’s test client — Django, Flask, or FastAPI.)
Custom tokens and links
If the code isn’t a plain number, pass your own regex with match and read matches; for magic links, use primaryLink:
python
# extract a custom token from "Your code: AB12-CD34"
email = wait_for_email(inbox["_id"], match=r"code: ([A-Z0-9-]+)")
token = email["matches"][0]
# follow a magic link
email = wait_for_email(inbox["_id"], subject="Confirm your email")
link = email["primaryLink"]Tips
/waitlong-polls, so therequestsread timeout must exceed it (the client adds 5s).- Unique prefix per test (millisecond timestamp) so
pytest-xdistparallel workers don’t share mail. - Reusing across steps? Pass
received_after=<ms>to skip stale mail. - Set
DEVMAILR_API_KEYas a CI secret.
Related reading
Try it in two minutes
Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.