The fastest way to send email in Python is with the standard library: smtplib + EmailMessage. Here is a minimal working example that connects to an SMTP server and sends a plain-text message:
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Hello from Python"
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg.set_content("This is a test email sent from Python.")
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login("[email protected]", "your-app-password")
smtp.send_message(msg)
That covers the hello-world case. For production use, most teams send through a transactional email service rather than raw SMTP. The sections below cover each method in order. Working in another stack? See the same walkthrough for sending email in Node.js.
Method 1: Send Plain-Text Email with smtplib
Python’s smtplib module handles the SMTP protocol. EmailMessage (from email.message) is the modern way to construct messages. Together they cover every basic sending case without any third-party package.
Port 587 (STARTTLS) vs. Port 465 (SSL)
Two ports are in common use:
- Port 587 with STARTTLS: opens a plain connection, then upgrades to TLS with the
starttls()call. This is the standard defined in RFC 6409 and the port Google recommends forsmtp.gmail.com. - Port 465 with implicit SSL: TLS handshake completes before any SMTP commands. Use
SMTP_SSLinstead ofSMTP.
Here is a full example using port 587 with STARTTLS:
import smtplib
import ssl
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Monthly report"
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg.set_content("Here is the monthly report. See attached.")
context = ssl.create_default_context()
with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
smtp.ehlo()
smtp.starttls(context=context)
smtp.ehlo()
smtp.login("[email protected]", "your-app-password")
smtp.send_message(msg)
And the equivalent using port 465 (implicit SSL):
import smtplib
import ssl
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Monthly report"
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg.set_content("Here is the monthly report.")
context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as smtp:
smtp.login("[email protected]", "your-app-password")
smtp.send_message(msg)
Gmail app passwords
Google requires an app-specific password when your account has 2-Step Verification enabled. Log in to your Google Account, go to Security, then “App passwords”, and generate a 16-character token. Use that token as the password in smtp.login(). Never hard-code credentials in source files; read them from environment variables instead:
import os
password = os.environ["GMAIL_APP_PASSWORD"]
Common errors
| Error | Cause | Fix |
|---|---|---|
SMTPAuthenticationError | Wrong credentials or app password not enabled | Generate an app password; enable 2-Step Verification |
ConnectionRefusedError | Wrong port or firewall blocking | Confirm port 587/465 is not blocked; try the other port |
SMTPNotSupportedError on starttls() | Server does not advertise STARTTLS | Switch to SMTP_SSL on port 465 |
Method 2: Send HTML Email with an Attachment
EmailMessage supports HTML bodies and file attachments directly, without building a MIMEMultipart object by hand.
import smtplib
import ssl
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Q2 report"
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
# Plain-text fallback (shown to clients that cannot render HTML)
msg.set_content("See the attached Q2 report. Open in a browser for the full version.")
# HTML alternative
msg.add_alternative(
"""\
<html>
<body>
<p>See the attached <strong>Q2 report</strong>.</p>
</body>
</html>
""",
subtype="html",
)
# Attach a PDF
with open("q2-report.pdf", "rb") as f:
msg.add_attachment(
f.read(),
maintype="application",
subtype="pdf",
filename="q2-report.pdf",
)
context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as smtp:
smtp.login("[email protected]", os.environ["GMAIL_APP_PASSWORD"])
smtp.send_message(msg)
Two things to note: set_content() sets the plain-text body, and add_alternative() adds the HTML version. Mail clients that support HTML will prefer it; others will fall back to the plain-text part. add_attachment() handles the MIME encoding automatically.
Method 3: Send via a Transactional Email API
Raw SMTP works fine for a handful of emails. At scale, or for critical messages like password resets and receipts, most teams switch to a transactional email service for three reasons:
- Deliverability: providers pre-warm IP pools and handle bounce/spam-complaint processing automatically.
- Reliability: built-in retry logic means a temporary downstream outage does not drop messages.
- Observability: open rates, bounce codes, and delivery events come back without building your own logging.
Most providers expose a simple HTTP API. The pattern is the same across all of them: authenticate with an API key, POST a JSON payload, and inspect the response.
import os
import requests
response = requests.post(
"https://api.your-provider.com/v1/email/send",
headers={
"Authorization": f"Bearer {os.environ['EMAIL_API_KEY']}",
"Content-Type": "application/json",
},
json={
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "Welcome to the app",
"text": "Thanks for signing up.",
"html": "<p>Thanks for signing up.</p>",
},
)
response.raise_for_status()
print(response.json())
Replace the endpoint URL and payload keys with your provider’s API reference. If you want templating, automation, and API sending in one tool, Coldletter is built for that use case.
Making Sure Email Lands in the Inbox
Switching to a provider API is only half the battle if your domain is not properly authenticated. The three DNS records that matter:
- SPF: a TXT record that lists the servers authorized to send on behalf of your domain. Example:
v=spf1 include:_spf.yourprovider.com ~all. - DKIM: a cryptographic signature your provider adds to outgoing messages. You publish the public key in DNS; recipients verify it.
- DMARC: a policy record (
p=none,p=quarantine, orp=reject) that tells receiving servers what to do when SPF or DKIM checks fail.
Without all three, major providers (Gmail, Outlook, Yahoo) are more likely to route your messages to spam. See Why Do My Emails Go to Spam? for a full diagnostic checklist.
One other rule: do not use a personal Gmail account to send bulk email. The daily SMTP limit for smtp.gmail.com is 500 messages for free accounts. For anything beyond that, use a verified sending domain through a transactional provider.
Frequently Asked Questions
How do I send email in Python without SMTP?
Use an HTTP-based email API. Most transactional email providers (Mailgun, Postmark, Resend, Sendgrid, Brevo, Coldletter) offer a REST API. You construct a JSON payload and POST it with Python’s requests library. No SMTP connection or port configuration needed.
Why is my Python email going to spam?
The most common causes are missing SPF/DKIM/DMARC records on your sending domain, sending from a shared IP with a poor reputation, or triggering spam filters with your content. Start by checking your domain’s DNS authentication records and reviewing common spam triggers.
How do I send email with Gmail in Python?
Use smtplib.SMTP("smtp.gmail.com", 587) with STARTTLS, or smtplib.SMTP_SSL("smtp.gmail.com", 465). Authentication requires an app password (a 16-character token from Google Account Security settings), not your regular Gmail password. Generate the token under Security > App passwords with 2-Step Verification enabled.
smtplib vs. a transactional email API: which should I use?
smtplib is the right choice for scripts, internal tooling, and low-volume notifications where deliverability monitoring is not needed. A transactional API is the right choice for user-facing email at any volume, where you need bounce handling, retry logic, open/click tracking, and reliable delivery to major inbox providers.
How do I send bulk email in Python?
Do not use smtplib for bulk sending. smtp.gmail.com caps free accounts at 500 messages per day. For bulk or broadcast email, use a transactional or marketing email API, verify your sending domain with SPF, DKIM, and DMARC, and manage list hygiene (unsubscribes, bounces) to protect your sender reputation.
Can I send email to multiple recipients at once?
Yes. Set msg["To"] to a comma-separated string or pass a list to send_message(). For BCC, set msg["Bcc"] before calling send_message(); send_message() strips the Bcc header automatically before transmission. For large recipient lists, use an API and send in batches with appropriate rate limits.
I’ve spent my career building software at scale with a soft spot for email: deliverability, lifecycle campaigns, and getting messages to actually land. I started Coldletter to fix what bugged me about transactional and marketing email tools. I’m based in Vancouver.
