Python is one of the most common languages for backend apps, scripts, and automation. And at some point, pretty much every project needs to send a transactional email.
This guide walks you through every practical method for doing that: Python's built-in smtplib for direct SMTP connections, the Brevo REST API through the official SDK, and raw HTTP requests for when you want zero dependencies. Each approach includes working code you can drop straight into your project.
Table of Contents
What you need to know first: SMTP and the email stack
Simple Mail Transfer Protocol (SMTP) is the standard protocol used to transmit email messages between servers. When you send emails using Python, you're either opening an SMTP connection directly with Python's built-in smtplib, or calling a REST API that handles the SMTP layer for you.
Multipurpose Internet Mail Extensions (MIME) is the standard that defines how email messages are structured. That means content type, encoding, attachments, and multi-part bodies. Python's email package implements MIME natively.
Before writing any code, you'll need a few things set up on the Brevo side.
- A verified sender address. In Brevo, go to Settings > Senders & IPs > Senders. Without this, every API call fails.
- An API key (for the REST API approach). Go to Settings > SMTP & API > API Keys > Create a new API key. Store it in an environment variable.
- An SMTP key (for the SMTP relay approach). Go to Settings > SMTP & API > SMTP Keys. This is a separate credential, not your API key and not your account password.
Method 1: Python smtplib + Brevo SMTP relay
If you'd rather send emails using Python's standard library, or if you're working in a framework that already uses SMTP (Django, Flask-Mail, legacy apps), Brevo's SMTP relay is a drop-in server. You configure import smtplib as you normally would, point it at Brevo's server, and authenticate with your SMTP key.
SMTP settings:
Migrating from Sendinblue? Update your hostname from smtp.sendinblue.com to smtp-relay.brevo.com.
Send a plain text email
Here's the minimal script to open an SMTP connection and send a plain text email message:
import smtplib
from email.message import EmailMessage
SMTP_SERVER = "smtp-relay.brevo.com"
SMTP_PORT = 587
SMTP_LOGIN = "[email protected]" # Your Brevo account email
SMTP_KEY = "YOUR_SMTP_KEY" # SMTP key - NOT your API key
msg = EmailMessage()
msg["Subject"] = "Your subject line here"
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg.set_content("Hello - this is a plain text email sent via Brevo SMTP.")
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.starttls() # Upgrade to a secure connection (TLS)
server.login(SMTP_LOGIN, SMTP_KEY)
server.send_message(msg)
print("Sent!")
EmailMessage (from email.message import EmailMessage) is the modern way to build an email message object in Python 3.6+. It handles encoding automatically and is cleaner than the older MIMEText approach.
Send an HTML email message
To send an HTML message body alongside a plain text fallback (which is best practice for email clients that don't render HTML), use MIMEMultipart:
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
SMTP_SERVER = "smtp-relay.brevo.com"
SMTP_PORT = 587
SMTP_LOGIN = "[email protected]"
SMTP_KEY = "YOUR_SMTP_KEY"
plain_text = "Hello! View this email in an HTML-compatible client for the best experience."
html_content = """\
<html>
<body>
<h1>Order confirmed</h1>
<p>Your order <strong>#12345</strong> has been received.</p>
</body>
</html>
"""
msg = MIMEMultipart("alternative")
msg["Subject"] = "Order Confirmation"
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
# Attach plain text first, HTML version last - email clients prefer the last part
msg.attach(MIMEText(plain_text, "plain"))
msg.attach(MIMEText(html_content, "html"))
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.starttls()
server.login(SMTP_LOGIN, SMTP_KEY)
server.send_message(msg)
print("Sent!")
Send to multiple recipients
To send the same email to multiple recipients in one SMTP connection, pass a list of email addresses to sendmail (or send_message). The To header accepts a comma-separated string:
import smtplib
from email.mime.text import MIMEText
recipients = ["[email protected]", "[email protected]", "[email protected]"]
msg = MIMEText("Team update: the release is live.", "plain")
msg["Subject"] = "Release announcement"
msg["From"] = "[email protected]"
msg["To"] = ", ".join(recipients)
with smtplib.SMTP("smtp-relay.brevo.com", 587) as server:
server.starttls()
server.login("[email protected]", "YOUR_SMTP_KEY")
server.sendmail("[email protected]", recipients, msg.as_string())
print("Sent!")
Privacy note: All recipients will see each other's addresses in the To field. For broadcast email, use BCC or send individually (see the batch section below).
Django configuration
For Django projects, add this to settings.py. No smtplib code required since Django handles the SMTP connection on its own:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp-relay.brevo.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = '[email protected]'
EMAIL_HOST_PASSWORD = 'YOUR_SMTP_KEY'
A note on Gmail
If you're testing locally with a Gmail account, you can use Gmail's SMTP server (smtp.gmail.com, port 587) with an App Password. You'll need to enable 2-factor authentication and generate an App Password first, because using your regular Gmail password directly will fail for security reasons.
That said, for anything going to production, a dedicated email service like Brevo is a much better fit. Gmail's shared sending limits are low, and deliverability gets unpredictable at volume. With your own SMTP server or a relay, you get proper analytics and bounce handling.
Method 2: Brevo REST API via Python SDK
For new projects, the REST API is the better choice. It's faster than opening an SMTP connection, gives you delivery analytics, supports templating, and handles rate limit headers natively. Brevo's official Python SDK (v4) covers all of this.
Install
pip install brevo-python
Version note: The pip package name brevo-python covers both v2 and v4 SDKs. The difference is the import. v4 uses from brevo import Brevo, while v2 uses import brevo_python. Use v4 for new projects.
Send your first email (synchronous)
import os
from brevo import Brevo
from brevo.transactional_emails import (
SendTransacEmailRequestSender,
SendTransacEmailRequestToItem,
)
client = Brevo(api_key=os.environ["BREVO_API_KEY"])
client.transactional_emails.send_transac_email(
subject="Order Confirmation",
html_content="<html><body><p>Your order #{{params.orderId}} is confirmed!</p></body></html>",
params={"orderId": "12345"},
sender=SendTransacEmailRequestSender(
email="[email protected]",
name="Your Company",
),
to=[
SendTransacEmailRequestToItem(
email="[email protected]",
name="John Doe",
)
],
)
print("Email sent!")
A successful call returns a messageId, which is a unique identifier for tracking delivery status in Brevo's transactional email logs.
Send emails using Python async (FastAPI / Starlette)
The v4 SDK ships AsyncBrevo for native async/await support, so you don't need thread pools:
import asyncio
import os
from brevo import AsyncBrevo
from brevo.transactional_emails import (
SendTransacEmailRequestSender,
SendTransacEmailRequestToItem,
)
client = AsyncBrevo(api_key=os.environ["BREVO_API_KEY"])
async def send_email():
await client.transactional_emails.send_transac_email(
subject="Password Reset",
html_content="<p>Click <a href='{{params.resetUrl}}'>here</a> to reset your password.</p>",
params={"resetUrl": "https://example.com/reset/abc123"},
sender=SendTransacEmailRequestSender(email="[email protected]", name="App"),
to=[SendTransacEmailRequestToItem(email="[email protected]", name="User")],
)
asyncio.run(send_email())
Error handling and rate limits
Brevo enforces roughly 600 emails/hour on paid plans. The API returns HTTP 429 with a Retry-After header when you go over that limit. The v4 SDK raises ApiError:
from brevo import Brevo
from brevo.core.api_error import ApiError
import time, os
client = Brevo(api_key=os.environ["BREVO_API_KEY"])
try:
client.transactional_emails.send_transac_email(...)
except ApiError as e:
if e.status_code == 429:
print(f"Rate limit exceeded. Retry after: {e.body}")
time.sleep(60)
elif e.status_code == 401:
print("Invalid API key")
elif e.status_code == 402:
print("Insufficient credits - upgrade your plan")
else:
print(f"API Error {e.status_code}: {e.body}")
For sustained high-volume sending, use exponential backoff:
def send_with_retry(email_params, max_retries=3):
for attempt in range(max_retries):
try:
return client.transactional_emails.send_transac_email(**email_params)
except ApiError as e:
if e.status_code == 429:
wait = 2 ** attempt * 10 # 10s, 20s, 40s
print(f"Rate limit hit. Waiting {wait}s (attempt {attempt+1}/{max_retries})...")
time.sleep(wait)
else:
raise
raise Exception("Max retries exceeded")
The SDK also supports built-in auto-retry with exponential backoff (default is 2 retries), and you can configure it at client initialization:
client = Brevo(api_key=os.environ["BREVO_API_KEY"], timeout=20.0)
Method 3: Raw HTTP with requests (no SDK)
If you want to skip the SDK entirely (useful in Lambda functions or minimal Docker images), the Brevo REST API is just a direct HTTP call. This is also a handy code snippet for testing the API without installing anything:
import requests
import json
import os
API_KEY = os.environ["BREVO_API_KEY"]
payload = {
"sender": {"email": "[email protected]", "name": "My App"},
"to": [{"email": "[email protected]", "name": "User"}],
"subject": "Welcome to My App!",
"htmlContent": "<html><body><h1>Hello!</h1></body></html>"
}
response = requests.post(
"https://api.brevo.com/v3/smtp/email",
headers={"api-key": API_KEY, "Content-Type": "application/json"},
data=json.dumps(payload)
)
if response.status_code == 201:
print("Sent! Message ID:", response.json()["messageId"])
elif response.status_code == 429:
retry_after = response.headers.get("Retry-After", "60")
print(f"Rate limit. Retry after {retry_after}s")
else:
print(f"Error {response.status_code}: {response.text}")
Pass your API key as the api-key header. A successful response is HTTP 201.
Send multiple personalized emails at scale
When you need to send multiple personalized emails (order confirmations, user notifications, onboarding sequences), don't loop over individual API calls. The Brevo API's messageVersions parameter lets you send up to 1,000 personalized email versions in a single API request. Each version can have its own recipient, subject, and template parameters.
For smaller lists, a simple loop with a def sendemail wrapper and rate-limit handling works fine:
import time
import os
from brevo import Brevo
from brevo.transactional_emails import SendTransacEmailRequestSender, SendTransacEmailRequestToItem
from brevo.core.api_error import ApiError
client = Brevo(api_key=os.environ["BREVO_API_KEY"])
sender = SendTransacEmailRequestSender(email="[email protected]", name="My App")
users = [
{"email": "[email protected]", "name": "Alice", "plan": "Pro"},
{"email": "[email protected]", "name": "Bob", "plan": "Starter"},
]
def sendemail(user):
client.transactional_emails.send_transac_email(
subject=f"Welcome to your {user['plan']} plan!",
html_content="<p>Hi {{params.name}}, your {{params.plan}} plan is active.</p>",
params={"name": user["name"], "plan": user["plan"]},
sender=sender,
to=[SendTransacEmailRequestToItem(email=user["email"], name=user["name"])],
)
for user in users:
try:
sendemail(user)
print(f"Sent to {user['email']}")
time.sleep(0.1) # Light throttle to stay within rate limits
except ApiError as e:
print(f"Failed for {user['email']}: {e.status_code}")
For anything above a few hundred emails per hour, use the batch messageVersions endpoint. The Brevo API reference has the full payload structure.
Legacy v2 SDK
If you're maintaining an older codebase using the v2 SDK (brevo_python module), the underlying API hasn't changed. Only the client interface is different:
import brevo_python
from brevo_python.rest import ApiException
configuration = brevo_python.Configuration()
configuration.api_key['api-key'] = 'YOUR_API_KEY'
api_instance = brevo_python.TransactionalEmailsApi(
brevo_python.ApiClient(configuration)
)
send_smtp_email = brevo_python.SendSmtpEmail(
to=[{"email": "[email protected]", "name": "Jane Doe"}],
sender={"name": "My App", "email": "[email protected]"},
subject="Welcome!",
html_content="<html><body><h1>Welcome!</h1></body></html>",
)
try:
response = api_instance.send_transac_email(send_smtp_email)
print(f"Message ID: {response.message_id}")
except ApiException as e:
print(f"Error: {e}")
Migrating to v4 is worth it for native async, Pydantic-typed models, and built-in exponential backoff. But v2 still works fine for existing projects.
Security checklist
A few practices worth applying to any Python email script before it goes to production.
- Never hardcode credentials. Use environment variables (os.environ["BREVO_API_KEY"]) or a secrets manager.
- Use TLS. Always call server.starttls() before server.login() on port 587. On port 465, use smtplib.SMTP_SSL instead of smtplib.SMTP.
- SMTP key ≠ API key. These are separate credentials in Brevo. Using the wrong one for the wrong method is the most common authentication failure.
- Verify your sender domain. Unverified senders will result in rejected or spam-filtered email messages.
What's next
The Brevo developer documentation covers the full API reference, webhook setup for delivery events (opens, clicks, bounces), template-based sending with templateId, and the full Python SDK reference.
Brevo's free plan is a practical starting point with 300 emails/day, full API access, and no credit card required. Try Brevo's email API and send your first email in under ten minutes.
For a broader comparison of email API providers, see Brevo's overview of the best email APIs and the complete email marketing API guide.







