If you searched for "python send email smtp", "send email python", or "smtplib python", the production-safe path is:

1. build messages with `EmailMessage`
2. send with `smtplib` using explicit TLS and authentication
3. keep SMTP settings in environment variables
4. verify inbox receipt, links, HTML, and attachments before release

Python can send a message in a few lines, but production email needs more than a successful `send_message()` call. You need to know that the email arrived, rendered correctly, and contained the expected links, codes, headers, and attachments.

![python smtp](/assets/python-smtp.jpg)

## Choose the right Python email path

| Need                                          | Recommended path                           | Why                                                           |
| --------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------- |
| Learn the SMTP basics                         | `smtplib` with a plain text message        | Smallest standard-library example.                            |
| Send transactional app email                  | `EmailMessage` plus authenticated SMTP     | Handles headers, MIME, HTML, and attachments cleanly.         |
| Run CI checks for signup, reset, or OTP email | MailSlurp inbox plus wait-for-email checks | Confirms the email actually arrived and matched expectations. |
| Prefer API-first sending                      | MailSlurp API or SDK                       | Easier to log, retry, and assert in automated workflows.      |

If you are deciding between SMTP and an HTTP email API, compare the tradeoffs in [SMTP vs HTTP email APIs](/guides/smtp-vs-http-email-inboxes/).

## Minimal smtplib example

This is the smallest useful SMTP example:

```python
import smtplib

message = "Subject: Welcome\r\n\r\nYour account is ready."

with smtplib.SMTP("smtp.example.com", 587, timeout=30) as smtp:
    smtp.starttls()
    smtp.login("smtp-user", "smtp-password")
    smtp.sendmail("no-reply@example.com", ["user@example.com"], message)
```

That is fine for learning, but production code should use structured messages, environment-backed settings, and receive-side assertions.

## Configure SMTP settings with environment variables

Use your deployment platform, shell, `.env` loader, or CI secret store to provide these values:

```bash
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=smtp-user
SMTP_PASSWORD=smtp-password
SMTP_FROM_ADDRESS=no-reply@example.com
SMTP_FROM_NAME="Example App"
```

Use port `587` with STARTTLS for most modern SMTP submission flows unless your provider documents another mode. Review [SMTP ports](/guides/what-are-smtp-ports/) and [STARTTLS vs SSL/TLS](/blog/starttls-ssl-tls/) when validating the setup.

## Send email with Python EmailMessage and smtplib

`EmailMessage` keeps headers and body content clearer than hand-built strings:

```python
import os
import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["From"] = os.environ["SMTP_FROM_ADDRESS"]
message["To"] = "recipient@example.com"
message["Subject"] = "Welcome"
message.set_content("Your account is ready.")

with smtplib.SMTP(os.environ["SMTP_HOST"], int(os.environ["SMTP_PORT"]), timeout=30) as smtp:
    smtp.starttls()
    smtp.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
    smtp.send_message(message)
```

Keep exception handling around the send call in application code so SMTP failures are logged and surfaced to your job runner.

## Send HTML email from Python

Add a plain text body first, then attach the HTML alternative:

```python
import os
import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["From"] = os.environ["SMTP_FROM_ADDRESS"]
message["To"] = "recipient@example.com"
message["Subject"] = "Reset your password"
message.set_content("Use the secure link in this email to reset your password.")
message.add_alternative(
    """
    <html>
      <body>
        <h1>Password reset</h1>
        <p>Use the secure link in this email to reset your password.</p>
      </body>
    </html>
    """,
    subtype="html",
)

with smtplib.SMTP(os.environ["SMTP_HOST"], int(os.environ["SMTP_PORT"]), timeout=30) as smtp:
    smtp.starttls()
    smtp.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
    smtp.send_message(message)
```

For release-critical HTML, run [email client testing](/testing/email-client-testing/) and [deliverability testing](/testing/email-deliverability-test/) before sending broadly.

## Send attachments from Python

Use `pathlib` and `mimetypes` so attachment handling is explicit:

```python
import mimetypes
from pathlib import Path

attachment_path = Path("reports/invoice.pdf")

if not attachment_path.is_file():
    raise FileNotFoundError(attachment_path)

content_type, _ = mimetypes.guess_type(attachment_path)
maintype, subtype = (content_type or "application/octet-stream").split("/", 1)

message.add_attachment(
    attachment_path.read_bytes(),
    maintype=maintype,
    subtype=subtype,
    filename=attachment_path.name,
)
```

After changing attachment code, send to a MailSlurp inbox and assert the expected filename, content type, and attachment count.

## Send email from Python with the MailSlurp API

For API-first tests or scheduled jobs, send from a MailSlurp inbox:

```python
import json
import os
import urllib.request

api_key = os.environ["MAILSLURP_API_KEY"]
inbox_id = os.environ["MAILSLURP_INBOX_ID"]

payload = json.dumps(
    {
        "to": ["recipient@example.com"],
        "subject": "Python API smoke test",
        "body": "Sent from a Python job through MailSlurp.",
    }
).encode("utf-8")

request = urllib.request.Request(
    f"https://api.mailslurp.com/inboxes/{inbox_id}/confirm",
    data=payload,
    method="POST",
    headers={
        "x-api-key": api_key,
        "Content-Type": "application/json",
    },
)

with urllib.request.urlopen(request, timeout=30) as response:
    sent_email = json.loads(response.read().decode("utf-8"))
```

This path is useful when you want API response data and test assertions without depending on a local SMTP session.

## Wait for the received email

For tests, wait for a MailSlurp inbox to receive the message and assert the content:

```python
import json
import os
import urllib.parse
import urllib.request

api_key = os.environ["MAILSLURP_API_KEY"]
receive_inbox_id = os.environ["MAILSLURP_RECEIVE_INBOX_ID"]

query = urllib.parse.urlencode(
    {
        "inboxId": receive_inbox_id,
        "timeout": 120000,
        "unreadOnly": "true",
    }
)

request = urllib.request.Request(
    f"https://api.mailslurp.com/waitForLatestEmail?{query}",
    headers={"x-api-key": api_key},
)

with urllib.request.urlopen(request, timeout=130) as response:
    email = json.loads(response.read().decode("utf-8"))

assert email["subject"] == "Python API smoke test"
assert "Python job" in email.get("body", "")
```

That check proves more than SMTP acceptance. It proves the workflow produced a readable email in the expected inbox.

## Common Python SMTP errors and fixes

### 535 authentication failed

Check credentials, app-password requirements, tenant policy, and sender-domain alignment. Then verify the from address is allowed by the SMTP provider. Use the [SMTP authentication guide](/blog/smtp-authentication/) for the full checklist.

### TLS or certificate failures

Confirm the port and TLS mode. Port `587` commonly expects `starttls()`, while port `465` uses SMTP over SSL. If you need SSL mode, use `smtplib.SMTP_SSL` and validate provider settings.

### Connection timed out

Check firewall rules, hosting-provider outbound SMTP restrictions, DNS, and proxy configuration. Add an explicit timeout so jobs fail clearly instead of hanging.

### Message accepted but never arrives

Inspect headers, SPF, DKIM, DMARC, spam score, blacklist status, and inbox placement:

- [Email header analyzer](/tools/email-header-analyzer/)
- [SPF checker](/tools/spf-checker/)
- [DKIM checker](/tools/dkim-checker/)
- [DMARC checker](/tools/dmarc-checker/)
- [Email blacklist checker](/tools/email-blacklist-checker/)
- [Email spam checker](/tools/email-spam-checker/)

## Production checklist for Python email

Before shipping Python email changes:

1. Keep SMTP and API secrets outside source code.
2. Use `EmailMessage` for headers, HTML, and attachments.
3. Validate port, TLS, authentication, and sender-domain policy.
4. Add timeouts and useful error logging around SMTP/API calls.
5. Send to a MailSlurp inbox and assert subject, body, links, codes, and attachments.
6. Re-test after template, DNS, or provider changes.
7. Use webhooks to track delivery, bounces, and downstream workflows.

Start with [Email Sandbox](/product/email-sandbox/) for isolated send-and-receive tests, [email integration testing](/product/email-integration-testing/) for CI assertions, and [email webhooks](/guides/email-webhooks/) for delivery evidence.

## FAQ

### Is smtplib enough to send email in production?

Yes for many workflows, if you use explicit TLS/auth settings, structured messages, timeouts, and receive-side assertions. The fragile part is usually not `smtplib`; it is untested configuration and deliverability.

### Can Python send HTML email?

Yes. Use `EmailMessage.set_content()` for the text body and `add_alternative(..., subtype="html")` for HTML.

### Can Python send attachments?

Yes. Use `EmailMessage.add_attachment()` with bytes, MIME type, and filename. Verify received attachments in a test inbox before release.

### How do I test Python email in CI?

Send to a MailSlurp inbox, call wait-for-email, and assert subject, body, links, OTP codes, headers, and attachments before the job passes.

## Next steps

- [SMTP and IMAP guide](/guides/smtp-imap/)
- [Email Sandbox setup](/product/email-sandbox/)
- [Email integration testing](/product/email-integration-testing/)
- [Deliverability testing workflow](/testing/email-deliverability-test/)
