MailSlurp logo

blog

How to Send Email in Python (SMTP + smtplib + Production Checklist)

Send email in Python with smtplib using secure SMTP settings, environment-based credentials, and release-ready testing workflows.

Python's built-in smtplib module can send a message through an SMTP provider without installing another sending library. Use EmailMessage to build the email, then connect with the provider's TLS and authentication settings.

The example below uses port 587 with required STARTTLS, certificate validation and a connection timeout. Send to a private test inbox first so you can inspect the actual result.

Quick setup with Python smtplib

Set SMTP_HOST, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM_ADDRESS and SMTP_TO_ADDRESS in your environment. The sender must be allowed by your SMTP provider. Use the exact generated address of your MailSlurp test inbox for SMTP_TO_ADDRESS.

import os
import smtplib
import ssl
from email.message import EmailMessage

msg = EmailMessage()
msg["Subject"] = "Python SMTP check"
msg["From"] = os.environ["SMTP_FROM_ADDRESS"]
msg["To"] = os.environ["SMTP_TO_ADDRESS"]
msg.set_content("This message checks the Python SMTP delivery path.")

context = ssl.create_default_context()
with smtplib.SMTP(os.environ["SMTP_HOST"], 587, timeout=30) as smtp:
    smtp.ehlo()
    smtp.starttls(context=context)
    smtp.ehlo()
    smtp.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
    refused = smtp.send_message(msg)
    if refused:
        raise RuntimeError(f"Recipients refused: {list(refused)}")

ssl.create_default_context() enables certificate validation using trusted certificate authorities and hostname checks. Do not replace a certificate failure with disabled verification. Check the hostname, trust store and provider setup instead.

Using implicit TLS on port 465

For a provider that documents port 465, replace the connection block with SMTP_SSL. TLS starts immediately, so do not call starttls() inside it:

with smtplib.SMTP_SSL(os.environ["SMTP_HOST"], 465, timeout=30, context=context) as smtp:
    smtp.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
    refused = smtp.send_message(msg)
    if refused:
        raise RuntimeError(f"Recipients refused: {list(refused)}")

Both examples use the same message and environment variables. The Python smtplib reference documents the two connection modes and exceptions. A missing STARTTLS capability must fail the 587 example before login.

What a successful send_message result proves

With multiple recipients, a send can succeed for some addresses and return a dictionary of refusals for others. If every recipient is refused, smtplib raises SMTPRecipientsRefused. Check the result instead of assuming a non-throwing call reached everybody.

An empty refusal dictionary means the SMTP server accepted all recipients in that transaction. It does not prove inbox placement. Open the receiving MailSlurp inbox and check the subject, body and sender; for automated checks, follow the Python SMTP send-and-receive guide. Test the application's real reset link or verification code before declaring that workflow complete.

SMTP settings Python teams should standardize

Before rollout, align on:

  • SMTP host and port
  • TLS mode (STARTTLS vs implicit TLS)
  • username/password rotation policy
  • sender identity alignment (SPF, DKIM, DMARC)
  • retry and timeout controls

References:

Use environment variables, not hardcoded secrets

import os

SMTP_HOST = os.environ["SMTP_HOST"]
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
SMTP_USERNAME = os.environ["SMTP_USERNAME"]
SMTP_PASSWORD = os.environ["SMTP_PASSWORD"]
SMTP_FROM_ADDRESS = os.environ["SMTP_FROM_ADDRESS"]

This improves staging/production parity and avoids credential leaks.

Common Python SMTP failures and fixes

Authentication errors (535, login denied)

Likely causes:

  • wrong credentials
  • auth-mode mismatch
  • sender-domain policy issues

Fix by validating credentials, SMTP auth mode, and sender-domain configuration together.

TLS or connection errors

Likely causes:

  • wrong port/TLS pairing
  • blocked outbound network path
  • certificate trust mismatch

Fix by validating port/protocol combinations and egress policy.

Message accepted by SMTP but not seen in inbox

SMTP success does not guarantee inbox placement.

Run diagnostics:

Test Python email flows before release

Production-safe checks should cover:

  • signup and verification email delivery
  • password reset link validity
  • OTP and time-sensitive message timing
  • template regression and header integrity

Recommended workflow pages:

Python SMTP production checklist

  1. Keep SMTP credentials and sender config in environment variables.
  2. Enforce SPF, DKIM, and DMARC alignment before release.
  3. Add inbox-based assertions for critical user journeys.
  4. Add retry, timeout, and error classification policies.
  5. Re-test after DNS, template, or provider changes.

FAQ

Is smtplib enough for production?

Yes, if you pair it with strong config management, delivery testing, and ongoing monitoring.

Should Python SMTP always use port 587?

Often yes with STARTTLS, but follow your provider requirements and validate per environment.

How do I test Python SMTP in CI?

Use isolated inboxes with deterministic receive assertions rather than checking only send responses.

Next steps