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:
multipart/mixed: The root container used when an email includes attachments alongside the body text.multipart/alternative: A sub-container nested withinmultipart/mixedthat holds different representations of the same content, such as a plain text fallback and an HTML version.- 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:
- Boundary Generation: It generates cryptographically
random delimiter strings (e.g.,
===============123456789==) to separate each MIME part. - Header Injection: It injects the appropriate
Content-Type: multipart/...; boundary="..."headers into parent containers. - Payload Encoding: Binary attachments are converted to ASCII using Base64 encoding with proper line wrapping to comply with RFC 5322 limits.
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 msgThis 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.