How to send email from Python: requests and smtplib examples
Two working ways to send email from Python on your own domain, an HTTP API with requests and SMTP with smtplib, plus timeouts, errors and safe retries.
Python gives you two sensible ways to send email from your own domain. One is an HTTPS request with the requests library. The other is the standard library's smtplib. Both work, and each has a situation where it's the better pick. Both examples below include the error handling most tutorials leave out.
Before you write code
You need a verified domain, because unauthenticated mail ends up in spam however well the code is written. Publish SPF, DKIM and DMARC first. What is an SPF record and what is DKIM explain them, and a quick check confirms they work.
Then pick the credential type:
- For the HTTP API, create an API key for the domain.
- For SMTP, create a mailbox such as
[email protected]and use its address and password.
Keep either one in an environment variable. Never commit it.
Option 1: the HTTP API with requests
Install the library with pip install requests, then:
import os
import requests
API_URL = os.environ["MAIL_API_URL"] + "/api/v1/emails"
class MailError(Exception):
pass
def send_email(to, subject, text, html=None, idempotency_key=None):
headers = {"Authorization": f"Bearer {os.environ['MAIL_API_KEY']}"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
payload = {
"from": "Your App <[email protected]>",
"to": to,
"subject": subject,
"text": text,
}
if html:
payload["html"] = html
r = requests.post(API_URL, headers=headers, json=payload, timeout=15)
data = r.json() if r.content else {}
if not r.ok:
err = data.get("error", {})
raise MailError(f"{err.get('code', r.status_code)}: {err.get('message', 'send failed')}")
return data # {"id": ..., "message_id": ..., "status": ...}
Two details matter here. The timeout=15 stops a stalled network from freezing a web worker; requests has no default timeout and will wait forever. And the check on r.ok makes failures loud rather than silent.
Call it like this:
send_email("[email protected]", "Welcome aboard", "Thanks for signing up.",
html="<p>Thanks for signing up.</p>", idempotency_key=f"welcome/{user.id}")
What the response status means
A 200 with status: accepted means the mail server took the message, not that the recipient's inbox did. A 202 with status: queued means the mail server was unavailable, so the message is stored and will be sent automatically for up to 24 hours. Don't send it again. Retrying a queued message creates a duplicate, which is the thing an idempotency key exists to prevent. Reuse the same key and body after a timeout, and no second copy is sent.
Errors worth handling specifically: rate_limited (wait a minute), monthly_limit_reached (the allowance is used up) and plan_required (no active trial or paid plan). The API guide lists them all.
Option 2: SMTP with smtplib
SMTP needs no extra package and works with any mail server. It also fits frameworks that expect SMTP settings, such as Django's EMAIL_* configuration.
import os
import smtplib
from email.message import EmailMessage
def send_smtp(to, subject, text):
msg = EmailMessage()
msg["From"] = "Your App <[email protected]>"
msg["To"] = to
msg["Subject"] = subject
msg.set_content(text)
with smtplib.SMTP("mail.example.com", 587, timeout=15) as smtp:
smtp.starttls()
smtp.login("[email protected]", os.environ["SMTP_PASSWORD"])
smtp.send_message(msg)
Port 587 with starttls() is the standard submission setup, and SMTP ports explained covers why. Replace mail.example.com with the host name your provider gives you.
If sending from a server fails with a connection timeout, the hosting company may block outgoing SMTP. That is the main reason to prefer the HTTP API for deployed apps.
Retrying without sending duplicates
Network calls fail. The question is which failures are safe to retry. Retry when the request never reached the server (a connection error or timeout) and when the API returns rate_limited. Do not retry a 202 queued, which already succeeded, or a 4xx error caused by bad input, which will fail the same way every time.
import time
def send_with_retry(*args, attempts=3, **kwargs):
for attempt in range(attempts):
try:
return send_email(*args, **kwargs)
except requests.RequestException:
if attempt == attempts - 1:
raise
time.sleep(2 ** attempt)
This is only safe if you pass an idempotency_key, so a request that actually succeeded before the timeout can't create a second email when it's repeated. Use a key that identifies the email, such as reset/ plus the reset request ID, never a random value generated per attempt.
Sending in the background
A web request shouldn't wait on email unless the page depends on it. For non-urgent messages, push the job to a worker such as Celery or RQ, or even a simple database table polled by a script, and let the worker call send_email. Keep the job small: store the recipient and a template name, not a rendered message, and render it when the worker runs.
Django and Flask
Django: set EMAIL_HOST, EMAIL_PORT = 587, EMAIL_USE_TLS = True, EMAIL_HOST_USER and EMAIL_HOST_PASSWORD, then use send_mail() as usual. It sends over SMTP.
Flask: use smtplib as above, or call the HTTP API from a small helper function. Either way, send from a background worker rather than inside the request when the email isn't needed for the page to render.
Choosing between them
| HTTP API | SMTP | |
|---|---|---|
| Works when SMTP ports are blocked | Yes | No |
| Clear status in the response | Yes | Limited |
| Idempotency keys | Yes | No |
| Works with frameworks expecting SMTP | No | Yes |
| Extra dependency | requests | None |
Test safely
While you build, send to a temporary inbox instead of real addresses. Pick any name, send, and watch the message appear in the browser. Free inboxes delete mail after 30 minutes and anyone who knows the address can read it, so use dummy content.
Next steps
If you also write Node.js, see sending email from Node.js. To send from your own domain with Mailions, contact us and we'll set up a trial.
Frequently asked questions
How do I send an email with Python?
Use the requests library to call an email API with a JSON body and an API key, or the built-in smtplib with EmailMessage over SMTP on port 587 with starttls(). Both work; an API gives clearer error responses.
Why does smtplib fail to connect?
Usually the wrong port or encryption: use port 587 with starttls(), or port 465 with SMTP_SSL. Many cloud servers also block outgoing SMTP ports, and an HTTPS email API avoids that problem.
How do I send email in Django or Flask?
Django has built-in email settings and send_mail(). Flask uses an extension such as Flask-Mail, or you call an email API directly. In both, send from a background task so pages stay fast.