How to Create Multipart MIME Emails in Python

Python's standard email package constructs multipart MIME (Multipurpose Internet Mail Extensions) messages by creating a hierarchical tree of message objects joined by unique boundary strings. This article explains how the package handles multipart formatting, how it transitions between different MIME types, and how to structure both message bodies and file attachments using the modern EmailMessage API.

The Hierarchy of Multipart MIME Messages

A standard email containing both formatted text and file attachments is structured as a tree rather than a flat document. The email package implements this structure through distinct MIME subtypes:

  1. multipart/mixed: The root container used when an email includes attachments alongside the body text.
  2. multipart/alternative: A sub-container nested within multipart/mixed that holds different representations of the same content, such as a plain text fallback and an HTML version.
  3. MIME Leaf Parts: The individual leaf nodes holding the actual data, such as text/plain, text/html, or binary application data (e.g., application/pdf, image/png).

Constructing the Message with EmailMessage

Python 3.6+ provides the email.message.EmailMessage class, which automates the boundary management and MIME type transitions that previously required manual handling with legacy classes like MIMEMultipart and MIMEBase.

1. Setting Headers and Plain Text

When an EmailMessage object is initialized, it starts as a simple, non-multipart message. Setting the initial text defines the base payload.

from email.message import EmailMessage

msg = EmailMessage()
msg["Subject"] = "Monthly Report"
msg["From"] = "sender@example.com"
msg["To"] = "recipient@example.com"
msg.set_content("Please find attached the monthly report.")

2. Adding an Alternative HTML Version

Calling add_alternative() transforms the message container into multipart/alternative. The plain text content provided in set_content() becomes the fallback part, while the HTML content is added as the preferred rendering option.

msg.add_alternative(
    """\
<!DOCTYPE html>
<html>
    <body>
        <p>Please find attached the <strong>monthly report</strong>.</p>
    </body>
</html>
""",
    subtype="html",
)

3. Appending File Attachments

When you call add_attachment(), the email package inspects the current structure. If the root message is already multipart/alternative, it automatically wraps the entire alternative section into a new multipart/mixed root container and places the attachment alongside it as a sibling.

The attachment requires raw binary data, the main MIME type, the MIME subtype, and a filename:

# Reading a binary file
file_data = b"Sample report content..."
file_name = "report.txt"

msg.add_attachment(
    file_data,
    maintype="text",
    subtype="plain",
    filename=file_name
)

For non-text files (like PDFs or images), the package automatically applies base64 Content-Transfer-Encoding and sets the Content-Disposition header to attachment; filename="...".

How the Package Handles Boundaries Under the Hood

When the message is serialized into bytes or a string using as_bytes() or as_string() (often right before sending via smtplib), the package performs several automated steps:

Complete Working Example

import mimetypes
from email.message import EmailMessage

def build_email():
    msg = EmailMessage()
    msg["Subject"] = "Quarterly Summary"
    msg["From"] = "finance@example.com"
    msg["To"] = "team@example.com"

    # 1. Plain text payload
    msg.set_content("This is the plain text version of the report.")

    # 2. HTML alternative
    msg.add_alternative("<p>This is the <b>HTML</b> version of the report.</p>", subtype="html")

    # 3. Read and attach a file
    filepath = "document.pdf"
    mime_type, _ = mimetypes.guess_type(filepath)
    maintype, subtype = (mime_type or "application/octet-stream").split("/", 1)

    with open(filepath, "rb") as f:
        msg.add_attachment(
            f.read(),
            maintype=maintype,
            subtype=subtype,
            filename=filepath
        )

    return msg

This sequence ensures the final message strictly adheres to MIME standards, allowing modern email clients to render the rich text while offering the attachment for download.