How To Send Email With Python

7 min read

Sending emails programmatically is a fundamental skill for any Python developer. So whether you are building a notification system for a web application, automating daily reports, or creating a contact form backend, understanding how to send email with Python unlocks powerful automation capabilities. Python’s standard library provides solid modules like smtplib and email that handle the heavy lifting, allowing you to focus on application logic rather than low-level protocol details.

Understanding the Core Components

Before diving into code, it helps to visualize the architecture. Sending an email involves three main actors: your Python script (the client), an SMTP server (the post office), and the recipient’s mail server. The Simple Mail Transfer Protocol (SMTP) is the standard communication protocol used to transmit the message from your script to the outgoing mail server Easy to understand, harder to ignore. Turns out it matters..

It sounds simple, but the gap is usually here.

Python’s smtplib module defines an SMTP client session object that encapsulates the connection to the server. Worth adding: messageoremail. Meanwhile, the email package (specifically email.mime) allows you to construct complex messages—complete with headers, HTML bodies, attachments, and alternative plain-text versions—without manually formatting raw RFC 5322 strings.

It sounds simple, but the gap is usually here.

Setting Up Your Environment

You do not need to install external packages for basic functionality; everything required ships with the standard library. Even so, for production applications involving templating or advanced attachment handling, libraries like yagmail or flanker can simplify syntax. For this guide, we will stick to the standard library to ensure zero dependencies and maximum portability Less friction, more output..

You will need access to an SMTP server. On top of that, common providers include:

  • Gmail: smtp. Still, gmail. com (Port 587 for TLS, 465 for SSL)
  • Outlook/Office 365: smtp.Plus, office365. com (Port 587)
  • SendGrid / Mailgun / Amazon SES: Transactional email APIs that also offer SMTP endpoints.

Security Note: Never hardcode passwords in your scripts. Use environment variables or a secrets manager. For Gmail, you must generate an App Password (requiring 2-Factor Authentication) rather than using your primary login password Easy to understand, harder to ignore..

Sending a Plain Text Email: The "Hello World"

Let’s start with the simplest possible script. This example connects via TLS (Transport Layer Security), authenticates, and sends a basic text message Turns out it matters..

import smtplib
import os
from email.message import EmailMessage

# Configuration (Ideally loaded from environment variables)
SMTP_SERVER = "smtp.gmail.com"
SMTP_PORT = 587
SENDER_EMAIL = os.getenv("EMAIL_USER")
SENDER_PASSWORD = os.getenv("EMAIL_PASS")
RECIPIENT_EMAIL = "recipient@example.com"

def send_plain_email():
    # 1. Create the message container
    msg = EmailMessage()
    msg["Subject"] = "Test Email from Python"
    msg["From"] = SENDER_EMAIL
    msg["To"] = RECIPIENT_EMAIL
    msg.set_content("This is a plain text email sent using Python's standard library.

    # 2. Connect and send
    try:
        with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
            server.starttls()  # Upgrade connection to encrypted TLS
            server.Plus, login(SENDER_EMAIL, SENDER_PASSWORD)
            server. send_message(msg)
        print("Email sent successfully!")
    except smtplib.SMTPAuthenticationError:
        print("Authentication failed. Check credentials/App Password.

if __name__ == "__main__":
    send_plain_email()

Key Takeaways:

  • EmailMessage handles header formatting automatically.
  • The with statement ensures the connection closes cleanly (sending QUIT command).
  • starttls() is critical for security on port 587; it encrypts the authentication and data transfer.

Crafting Rich HTML Emails

Modern applications rarely send plain text alone. You likely want formatted text, links, images, and branding. The EmailMessage class supports add_alternative(), allowing you to attach an HTML version alongside the plain text fallback. Email clients will render the best version they support.

def send_html_email():
    msg = EmailMessage()
    msg["Subject"] = "Weekly Newsletter"
    msg["From"] = SENDER_EMAIL
    msg["To"] = RECIPIENT_EMAIL

    # Plain text fallback
    text_content = "Hi there,\n\nCheck out our latest updates!\n\nThanks,\nThe Team"
    msg.set_content(text_content)

    # HTML version
    html_content = """
    
      
        

Weekly Newsletter

Hi there,

Check out our latest updates!


Thanks,
The Team

""" msg. # Sending logic remains the same with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: server.starttls() server.login(SENDER_EMAIL, SENDER_PASSWORD) server. Using `add_alternative` creates a `multipart/alternative` MIME structure. This is the industry standard for ensuring deliverability and accessibility. ## Adding Attachments Reports, invoices, logs, or images are common attachment requirements. Practically speaking, the `EmailMessage` object treats attachments as additional parts of a `multipart/mixed` container. You read the file in binary mode, determine the MIME type (using `mimetypes` module), and add it via `add_attachment`. ```python import mimetypes def send_email_with_attachment(file_path): msg = EmailMessage() msg["Subject"] = "Monthly Report Attached" msg["From"] = SENDER_EMAIL msg["To"] = RECIPIENT_EMAIL msg.set_content("Please find the attached monthly report.") # Guess MIME type based on file extension mime_type, _ = mimetypes.guess_type(file_path) if mime_type is None: mime_type = "application/octet-stream" # Generic binary fallback main_type, sub_type = mime_type.split("/") with open(file_path, "rb") as f: file_data = f.Practically speaking, read() file_name = os. Also, path. basename(file_path) msg. with smtplib.starttls() server.SMTP(SMTP_SERVER, SMTP_PORT) as server: server.login(SENDER_EMAIL, SENDER_PASSWORD) server. This approach works for PDFs, CSVs, PNGs, and any binary file. For inline images (embedded in HTML via ``), you would use `add_related` with a Content-ID header, but standard attachments use `add_attachment`. ## Using SSL (Port 465) vs. TLS (Port 587) You will encounter two primary port standards. Think about it: * **Port 587 (STARTTLS):** The connection starts unencrypted. Think about it: the client issues `STARTTLS` command to upgrade to TLS. This is the modern standard for **message submission** (RFC 6409). Now, * **Port 465 (SMTPS):** The connection is encrypted immediately via SSL/TLS upon socket connection. This was the original secure submission port. Not obvious, but once you see it — you'll see it everywhere. While Port While Port 587 is the modern standard for message submission, some legacy mail servers or specific hosting providers still require Port 465 (SMTPS). Supporting both options makes your script more adaptable to different environments. Below is a flexible helper that chooses the appropriate port and encryption method based on a simple flag. ```python import smtplib from email.message import EmailMessage import os def send_email( subject: str, body: str, recipient: str, sender: str, use_tls: bool = True, port: int = 587, attachments: list[str] | None = None, ) -> None: """ Send an email with optional attachments. """ msg = EmailMessage() msg["Subject"] = subject msg["From"] = sender msg["To"] = recipient msg.set_content(body) Short version: it depends. Long version — keep reading. if attachments: import mimetypes for path in attachments: mime_type, _ = mimetypes.guess_type(path) mime_type = mime_type or "application/octet-stream" main_type, sub_type = mime_type.split("/", 1) with open(path, "rb") as f: data = f.read() filename = os.path.basename(path) msg. # SMTP configuration – prefer environment variables for secrets smtp_server = os.Now, getenv("SMTP_SERVER", "smtp. So example. com") smtp_user = os.getenv("SMTP_USER", sender) smtp_pass = os. if use_tls: with smtplib.That said, sMTP(smtp_server, port) as server: server. starttls() server.login(smtp_user, smtp_pass) server.On the flip side, send_message(msg) else: # Direct SSL (Port 465) with smtplib. SMTP_SSL(smtp_server, port) as server: server.login(smtp_user, smtp_pass) server. print(f"Email sent to {recipient}")

Security & Reliability Tips

  • Never hard‑code credentials. Store SMTP_USER and SMTP_PASS (or OAuth2 tokens) in environment variables or a secure vault And it works..

  • Validate recipient addresses before sending to avoid bounce‑back penalties.

  • Catch SMTP exceptions to provide meaningful feedback:

    try:
        send_email(...)
    except smtplib.SMTPException as e:
        print(f"Failed to send email: {e}")
    
  • Rate limiting: If you are sending bulk emails, pause between batches (e.g., time.sleep(1)) to respect the sending quotas of most providers.

  • TLS vs. SSL:

TLS vs. SSL: Use starttls() (Port 587) whenever possible; it upgrades a plaintext connection to encrypted, which is the modern standard (RFC 3207). Reserve SMTP_SSL (Port 465) for legacy servers that require immediate encryption upon connection It's one of those things that adds up..

  • Retry logic: Transient network glitches or greylisting can cause temporary failures. Implement exponential backoff (e.g., via tenacity or a simple loop) for mission-critical notifications.

  • DKIM/SPF/DMARC alignment: Ensure your sending domain publishes valid SPF and DKIM records and that the From header aligns with them. Without this, even well‑formed messages land in spam folders.

  • HTML alternatives: For richer formatting, add an HTML part alongside the plain‑text body:

    msg.add_alternative(
        f"

    {body.replace(chr(10), '
    ')}

    ", subtype="html", )

Conclusion

Sending email programmatically remains a foundational skill for developers, yet it is fraught with protocol nuances, security pitfalls, and deliverability challenges. By encapsulating the logic in a reusable, type‑hinted function—one that respects modern starttls standards while gracefully falling back to implicit SSL—you gain a reliable building block that works across cloud functions, containerized services, and on‑premise scripts alike Simple as that..

Remember that the code is only half the battle. Securing credentials through environment variables, honoring provider rate limits, implementing retry strategies, and configuring proper DNS authentication records are what separate a "script that sends mail" from a production‑grade notification pipeline. With the helper above and the operational guardrails outlined here, you are equipped to integrate email into your applications confidently, securely, and at scale No workaround needed..

This is the bit that actually matters in practice.

Just Hit the Blog

Trending Now

Picked for You

Picked Just for You

Thank you for reading about How To Send Email With Python. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home