Send email with Python (smtplib) over SMTP
Complete Python 3 example: smtplib with STARTTLS on 587 (or SMTP_SSL on 465), EmailMessage with HTML + text alternative and an attachment, credentials from environment variables, error handling and a reusable send() function.
Install
# standard library only — nothing to install
Complete example
import os, smtplib, ssl
from email.message import EmailMessage
HOST = os.environ.get("SMTP_HOST", "smtp.queensmtp.com")
PORT = int(os.environ.get("SMTP_PORT", "587"))
USER = os.environ["SMTP_USER"]
PASS = os.environ["SMTP_PASS"]
def send(to: str, subject: str, text: str, html: str | None = None, attachment: str | None = None) -> None:
msg = EmailMessage()
msg["From"] = "Your App <hello@yourdomain.com>" # verified sending domain
msg["To"] = to
msg["Subject"] = subject
msg.set_content(text)
if html:
msg.add_alternative(html, subtype="html")
if attachment:
with open(attachment, "rb") as f:
msg.add_attachment(f.read(), maintype="application", subtype="pdf",
filename=os.path.basename(attachment))
ctx = ssl.create_default_context()
if PORT == 465: # implicit TLS
with smtplib.SMTP_SSL(HOST, PORT, context=ctx, timeout=20) as s:
s.login(USER, PASS)
s.send_message(msg)
else: # STARTTLS on 587 / 2525
with smtplib.SMTP(HOST, PORT, timeout=20) as s:
s.ehlo()
s.starttls(context=ctx)
s.login(USER, PASS)
s.send_message(msg)
if __name__ == "__main__":
send("customer@example.com", "Your receipt", "Thanks for your order #1042",
"<h1>Thanks for your order</h1><p>#1042 is confirmed.</p>")What to know
- Always call starttls() before login() on 587 — logging in on a plain connection sends the password in clear text and most servers refuse it (530 5.7.0 Must issue a STARTTLS command first).
- Reuse one connection for a batch: open, login, loop send_message(), quit. Do not reconnect per message.
- smtplib.SMTPAuthenticationError (535) means the credentials; smtplib.SMTPRecipientsRefused (550/553) means the To or From address was rejected — log both separately.
- Django users: see the Django page; it wraps this in settings.EMAIL_*.
Same code, different provider
Only the host, port and credentials change. The rules per provider are on the SMTP settings hub.
| Provider | Host | Port | Username | Password | Note |
|---|---|---|---|---|---|
| QueenSMTP | smtp.queensmtp.com | 587 (STARTTLS) / 465 (SSL) / 2525 | SMTP username from the dashboard | SMTP password | $5/year + $0.10 per 1,000; 100 free/day |
| Gmail | smtp.gmail.com | 587 / 465 | you@gmail.com | App Password | 500/day, From must be the account |
| Amazon SES | email-smtp.<region>.amazonaws.com | 587 / 465 / 2587 | SES SMTP username | SES SMTP password | sandbox 200/day until approved |
| Office 365 | smtp.office365.com | 587 | user@yourdomain.com | mailbox password | SMTP AUTH must be enabled; 30/min |
| SendGrid | smtp.sendgrid.net | 587 / 465 / 2525 | apikey | API key | no free plan since 2025 |
Credentials for the example above
Create a free account, verify your sending domain (SPF + DKIM shown in the dashboard), add an SMTP credential, and paste it into the code. 100 emails a day free; $5/year plus $0.10 per 1,000 after that. Prefer HTTPS? The same account has a REST email API.
Get SMTP credentials Test your settings firstOther languages and frameworks
Python SMTP — questions
Create an EmailMessage, set_content() with the plain text, then add_alternative(html, subtype="html"). Receivers show the HTML and keep the text for spam scoring and accessibility.
The server rejected the username/password. For Gmail that means an App Password is required; for a relay, check the SMTP credential in the dashboard.
Related Guides
Continue learning with these related articles