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

shell
# standard library only — nothing to install

Complete example

Python — STARTTLS on 587, credentials from the environment
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.

ProviderHostPortUsernamePasswordNote
QueenSMTP smtp.queensmtp.com587 (STARTTLS) / 465 (SSL) / 2525SMTP username from the dashboardSMTP password$5/year + $0.10 per 1,000; 100 free/day
Gmail smtp.gmail.com587 / 465you@gmail.comApp Password500/day, From must be the account
Amazon SES email-smtp.<region>.amazonaws.com587 / 465 / 2587SES SMTP usernameSES SMTP passwordsandbox 200/day until approved
Office 365 smtp.office365.com587user@yourdomain.commailbox passwordSMTP AUTH must be enabled; 30/min
SendGrid smtp.sendgrid.net587 / 465 / 2525apikeyAPI keyno 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 first

Other languages and frameworks

Examples tested 2026-08-30. If a library changed its API, tell us.

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.