You probably found this tutorial because you want to send emails with Python to automate confirmation messages, password resets, or scheduled notifications. Python’s standard library covers the whole pipeline, from making a server connection to building the message and sending it to one or many recipients. This tutorial walks through every step in working code.
By the end of this tutorial, you’ll understand that:
- A safe testing setup uses a throwaway Gmail account with an app password, a local
aiosmtpddebug server, or a privacy-focused provider like Posteo or Proton Mail. - A secure SMTP session uses
.SMTP_SSL()withssl.create_default_context(), which validates the server certificate and encrypts your credentials and message content. - The
EmailMessageclass from theemailpackage assembles plain text, HTML alternatives, file attachments, and personalized fields through.set_content(),.add_alternative(), and.add_attachment(). - Setting
msg["reply-to"]or any other RFC 5322 header on anEmailMessageroutes replies to a different mailbox than the sender address. - For high-volume sending, transactional email services like SendGrid, Mailgun, and Brevo provide deliverability, statistics, and API libraries that go beyond what
smtplibalone offers.
Before you jump into the code, you’ll set up a throwaway email account or a local debug server so you can experiment freely without spamming real inboxes.
Get Your Code: Click here to download the free sample code you’ll use to learn how to send plain-text and HTML emails, attach files, and automate email delivery with Python.
Take the Quiz: Test your knowledge with our interactive “Sending Emails With Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Sending Emails With PythonUse Python's standard library to send email through secure SMTP connections, attach files, include HTML content, and route replies.
Setting Up an Email Service
Email is sent from a client to an email server, and from one email server to another, using the Simple Mail Transfer Protocol, or SMTP, defined under RFC 821. Python comes with the built-in smtplib module, which implements this protocol, allowing you to programmatically send email through any accessible email server.
While you can certainly use your own email account for this tutorial, it’s recommended that you set up a throwaway email account instead. There are several free and paid email services you can use. In this tutorial, you’ll explore the following options:
- Setting up a Gmail account for development: You’ll learn how to create a dedicated testing account and use app passwords to satisfy modern security requirements.
- Setting up a local SMTP server: You’ll use the
aiosmtpdlibrary to run a server on your own machine, allowing you to inspect email content without sending any live messages. - Setting up other email accounts for development: You’ll see how to connect to alternative services like Posteo or Proton Mail to ensure your code works across different providers.
Understanding the distinction between secure (encrypted) and insecure (unencrypted) connections is vital. Most modern providers require encryption via SSL or TLS to protect your data, while the local debugging server uses no encryption. By the end of this section, you’ll know how to choose the right connection type for your specific service choice.
Setting Up a Gmail Account for Development
To set up a Gmail account for testing your code, follow these steps:
- Create a new Google account. You need to provide a name, a birthday, and a unique username for the account.
- Set up two-factor authentication for the new account.
- Add a new app password to allow password sign-ins to the account.
An app password is a temporary password generated by Google. Instead of using your main account password to authenticate with your username, you use the app password. You can delete and recreate app passwords whenever you like.
App passwords allow access to Gmail when modern security measures like OAuth2 aren’t available. When creating one, make sure you copy it to a secure location, as you won’t be able to review it after leaving the page.
If you don’t want to use an app password, check out Google’s documentation on how to obtain access credentials for your Python script using the OAuth2 authorization framework.
A nice feature of Gmail is that you can use the + sign to add modifiers to your email address right before the @ sign. For example, emails sent to my+person1@gmail.com and my+person2@gmail.com will both arrive at my@gmail.com. When testing email functionality, you can use this to simulate multiple addresses that all point to the same inbox.
Setting Up a Local SMTP Server
You can test email functionality by running a local Simple Mail Transfer Protocol (SMTP) debugging server with the aiosmtpd module. Rather than sending emails to a specific address, the local debug server discards the message after printing its content to the console. Running a local debugging server makes it unnecessary to deal with encryption of messages or use credentials to log in to an email server.
Note: aiosmtpd is a third-party library that replaces the former built-in smtpd module, which was initially deprecated in Python 3.4.7. Deprecation notices were repeated in 3.5.4 and 3.6.1, and the module was eventually removed in Python 3.12, as outlined in PEP 594.
Install the aiosmtpd module with the following command:
$ python -m pip install aiosmtpd
Then, start a local SMTP debugging server with this command:
$ python -m aiosmtpd -n
The server runs by default on localhost, at port 8025. Any emails sent to this server are printed to the terminal. The debug server doesn’t implement any authentication or security, making it perfect for debugging.
Setting Up Other Email Accounts for Development
You can, of course, use your own email service and account for testing. The only requirement is that the account must allow SMTP access. Some options are listed below:
- Proton Mail is a privacy-first, end-to-end encrypted, open-source email service based in Switzerland. It offers limited free email accounts perfect for testing, as well as paid offerings with more storage and features, such as email aliases, custom domains, and VPN support. One disadvantage is that Proton Mail only allows SMTP access through paid accounts, using its Proton Mail Bridge software.
- Posteo is based in Germany and provides low-cost email access using 100% green and sustainable energy. Like Proton, it’s a privacy-first encrypted service that you can expand with additional storage and features. As with Google, you can create app passwords to authenticate and send email.
Of course, there are many other email services available. Consult your email provider’s documentation on how to connect to it via SMTP.
Connecting to an Email Service in Python
Now that you have a server in place to send email, you need to make a connection to that server before smtplib can deliver any messages.
As mentioned earlier, production email services like Gmail require an encrypted connection over SSL or TLS, while the local debugging server from aiosmtpd accepts unencrypted connections by default. You’ll learn how to create both kinds of connections in the subsections below, then wrap them in a single helper function to reuse throughout the rest of the tutorial.
Starting a Secure SMTP Connection
When you send emails using Python, you make an encrypted connection to the server via SMTP, which ensures your message and login credentials aren’t easily accessed by others. SSL (Secure Sockets Layer) and the more recent TLS (Transport Layer Security) are protocols used to encrypt an SMTP connection. It’s not necessary to use either of these when using a local debugging server.
According to Python’s security considerations, it’s highly recommended that you use .SMTP_SSL() with an SSL context created with create_default_context() from the ssl module. This loads the system’s trusted CA certificates, enables hostname checking and certificate validation, and tries to choose reasonably secure protocol and cipher settings.
No matter how you connect, Gmail encrypts emails using TLS, as this is the more secure successor of SSL. To check the encryption for any email in your Gmail inbox, open the email, click More ⋮, then Show original to see the encryption type listed under one of the Received headers:
Received: from sender.email.server.com (sender.email.server.com. [192.168.1.2])
by mx.google.com with ESMTPS id ABC12345
for <email@address.com>
(version=TLS1_2 cipher=ECDHE-ECDSA-CHACHA20-POLY1305 bits=256/256);
Wed, 01 Apr 2026 04:07:59 -0700 (PDT)
Check with your email provider to see what encryption protocols they implement when connecting to the server.
Using .SMTP_SSL() creates an SSL connection to the server that’s encrypted from the start. Using ssl.create_default_context() defines the purpose of the connection as server authentication, which loads default certificates to validate the host and optimize the security settings.
Make sure to fill in your own email address instead of my@gmail.com:
1import smtplib
2import ssl
3from getpass import getpass
4
5port = 465
6smtp_server = "smtp.gmail.com"
7sender_email = "my@gmail.com"
8password = getpass("Type your password and press enter: ")
9
10context = ssl.create_default_context()
11with smtplib.SMTP_SSL(
12 smtp_server,
13 port,
14 context=context,
15 ) as server:
16 server.login(sender_email, password)
17 # Send email here
If port is zero or not specified, .SMTP_SSL() will use port 465, which is the standard port for SMTP over SSL.
It’s not a best practice to store your email password in your code, especially if you intend to share it with others or check it into source control. Using getpass() allows you to enter your password without echoing it to the screen. You can also use other methods to hide it, such as storing it in an environment variable and retrieving it with os.getenv().
Starting an Insecure SMTP Connection
An insecure SMTP connection—one created with smtplib.SMTP() and without TLS—is only appropriate for the debug server provided by aiosmtpd. It’s unencrypted, so anyone on your network can monitor the traffic:
1port = 8025
2smtp_server = "localhost"
3sender_email = "my@gmail.com"
4receiver_email = "your@gmail.com"
5
6with smtplib.SMTP(smtp_server, port) as server:
7 # Send email here
8 pass
Note that there’s no security context created or used here, and no need to log in to the debug server.
Using a debug server during development is helpful, but switching to an encrypted server later requires small but crucial changes to your code. To make switching more robust and predictable, the code examples in this tutorial define and use the following function, which uses the debug server by default but can be changed to use any other email server:
send_msg.py
1import smtplib
2import ssl
3from getpass import getpass
4
5def send(msg, sender_email, debug=True):
6 if debug:
7 smtp_server = "localhost"
8 port = 8025
9 with smtplib.SMTP(smtp_server, port) as server:
10 server.send_message(msg)
11
12 else:
13 smtp_server = "smtp.gmail.com"
14 port = 465
15 password = getpass("Type your password and press enter: ")
16
17 context = ssl.create_default_context()
18 with smtplib.SMTP_SSL(
19 smtp_server, port, context=context
20 ) as server:
21 server.login(sender_email, password)
22 server.send_message(msg)
To use an email server other than Gmail, change the smtp_server on line 13 and optionally update the port on line 14. Consult your email server documentation for the correct values of these variables. This send() function is used throughout the tutorial, and you can switch debug between True and False to see how it works.
Sending Email in Python Using the email Package
You’ll start with plain-text emails in this section, then build up to HTML content and attachments later in the tutorial.
Email began in the 1960s as text-only messages exchanged between accounts on a single mainframe. In 1971, Ray Tomlinson sent the first email between two different computers on ARPANET, using the now-familiar @ sign to designate the user’s address.
Throughout the 1970s and into the early 1980s, proprietary email systems were available from companies such as IBM, Xerox, and CompuServe for use on closed networks. The SMTP protocol arrived in the early 1980s and became the standard protocol for internet email, with extensions formalized in the mid-1990s.
For a more in-depth history, visit the History of email page on Wikipedia.
While you can certainly send email using just SMTP, Python’s real power comes with the standard library email package. Its EmailMessage class handles the basics like recipients, subject lines, and body content, as well as attachments, HTML alternatives, and custom headers.
The EmailMessage class is used to encapsulate everything about an email message. This includes the basics like the recipient, subject line, and the message itself, but also more advanced things like attachments, alternative versions, and email headers.
Think of an EmailMessage object conceptually as a dictionary, where email headers are defined by RFC 5322 as keys, and the values are strings. These are combined with a payload, which represents the message body, but may also contain sub-EmailMessage objects for alternative versions and attachments.
The email package doesn’t send or receive email, so you need to use smtplib to send your messages.
Sending Plain-Text Email
You’ll start with the basics by sending a plain-text message using EmailMessage:
plain_text_email.py
1from send_msg import send
2from email.message import EmailMessage
3
4sender_email = "my@gmail.com"
5receiver_email = "your@gmail.com"
6
7# Build Email Message
8msg = EmailMessage()
9msg["to"] = receiver_email
10msg["from"] = sender_email
11msg["subject"] = "Test Message"
12msg.set_content("This is a test message")
13
14# Send message
15send(msg, sender_email)
To use EmailMessage, you need to import it on line 2. Then, you create a new EmailMessage on line 8 and set the to, from, and subject fields using standard dictionary syntax. The body of the email, however, is set with .set_content() on line 12.
Running this code on the debug server results in the following output:
---------- MESSAGE FOLLOWS ----------
to: your@gmail.com
from: my@gmail.com
subject: Test Message
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 55872, 0, 0)
This is a test message
------------ END MESSAGE ------------
Along with the data you specified, the headers Content-Type, Content-Transfer-Encoding, MIME-Version, and X-Peer are also present in the message that was sent. The first three headers are automatically added by the EmailMessage class, and you can override them as you wish. In fact, you’ll be manipulating Content-Type to send HTML messages next. The X-Peer header is provided by aiosmtpd and may vary depending on your system setup.
Including HTML Content
HTML emails allow you to format your message with features like bold and italics, and include images, hyperlinks, or other responsive content. Most emails sent today consist of HTML content.
Note: While HTML supports advanced features like CSS and tracking pixels, it also introduces security risks. Many email clients block scripts and remote content by default to protect users from malicious code.
Because some recipients explicitly opt for text-only emails or use high-security clients that block HTML, you should always provide a plain-text alternative. You can use the .add_alternative() method to attach a second version to your message:
html_email.py
1from send_msg import send
2from email.message import EmailMessage
3
4sender_email = "my@gmail.com"
5receiver_email = "your@gmail.com"
6
7# Build Email Message
8msg = EmailMessage()
9msg["to"] = receiver_email
10msg["from"] = sender_email
11msg["subject"] = "HTML Test Message"
12
13text = """\
14Hi,
15How are you?
16Real Python has many great tutorials:
17realpython.com"""
18
19html = """\
20<html>
21 <body>
22 <p>Hi,<br>
23 How are you?<br>
24 <a href="https://realpython.com">Real Python</a>
25 has many great tutorials.
26 </p>
27 </body>
28</html>
29"""
30
31msg.set_content(text)
32msg.add_alternative(html, subtype="html")
33
34# Send message
35send(msg, sender_email)
The setup is similar to that of a normal text message, but the HTML portion is set after the plain-text portion using .add_alternative() on line 32. Most HTML-capable email clients display the last alternative as the preferred version, so this ensures that the HTML payload takes precedence over the plain-text fallback.
When adding alternative content, the subtype html is used by EmailMessage to properly set the Content-Type header for that payload:
1---------- MESSAGE FOLLOWS ----------
2to: your@gmail.com
3from: my@gmail.com
4subject: HTML Test Message
5MIME-Version: 1.0
6Content-Type: multipart/alternative;
7 boundary="===============8170693437571293663=="
8X-Peer: ('::1', 46768, 0, 0)
9
10--===============8170693437571293663==
11Content-Type: text/plain; charset="utf-8"
12Content-Transfer-Encoding: 7bit
13
14Hi,
15How are you?
16Real Python has many great tutorials:
17realpython.com
18
19--===============8170693437571293663==
20Content-Type: text/html; charset="utf-8"
21Content-Transfer-Encoding: 7bit
22MIME-Version: 1.0
23
24<html>
25 <body>
26 <p>Hi,<br>
27 How are you?<br>
28 <a href="https://realpython.com">Real Python</a>
29 has many great tutorials.
30 </p>
31 </body>
32</html>
33
34--===============8170693437571293663==--
35------------ END MESSAGE ------------
Note that the Content-Type on line 6 for the entire message is set to multipart/alternative, with a boundary property on line 7. The Content-Type specifies this is a multipart message, and the boundary separates the text and HTML payloads. Also, each payload has its own Content-Type. The plain-text version is set on line 11, while the HTML version is set on line 20.
Handling Attachments, Headers, and Recipients
Plain-text and HTML emails cover most everyday cases, but real-world email often needs more, like file attachments, multiple recipients, and custom headers. This section covers all three.
Adding Attachments
Attachments allow your emails to carry more than text, like documents, images, spreadsheets, or anything else your recipient needs, alongside the message body.
The problem with attachments is that email is a text-only medium, and these files are usually binary. Sending binary files to an email server requires them to be encoded beforehand. This encoding is done automatically with .add_attachment().
The code example below shows how to send an email with a small JPG file as an attachment. You can download this image locally by clicking the “Free Download” button below the picture. You’ll also find the image in the tutorial’s downloads:
attachment_email.py
1from send_msg import send
2from email.message import EmailMessage
3from pathlib import Path
4
5sender_email = "my@gmail.com"
6receiver_email = "your@gmail.com"
7
8# Build Email Message
9msg = EmailMessage()
10msg["to"] = receiver_email
11msg["from"] = sender_email
12msg["subject"] = "Attachment Test Message"
13
14text = "Please find a JPG attached."
15msg.set_content(text)
16
17attachment_file = Path("smiley-small.jpg")
18with open(attachment_file, "rb") as attachment:
19 # Add attachment to message
20 msg.add_attachment(
21 attachment.read(),
22 maintype="image",
23 subtype="jpeg",
24 filename=attachment_file.name,
25 )
26
27# Send message
28send(msg, sender_email)
A plain-text email is added to the message on line 15 using .set_content(). Next, the file to be attached is opened in binary mode, and the contents of the file are read as the first argument in the .add_attachment() call on line 20.
When attaching the file, you need to identify the type of the file being attached. Email clients can then read the type and provide proper handling of the file. In this case, you need to specify the main file type as an image, with a subtype of jpeg. For more information, expand the collapsible section on MIME below.
MIME (Multipurpose Internet Mail Extensions) is a standard method for referencing, encoding, and decoding content in formats other than ASCII used in email messages. Originally devised for email, MIME is also used in HTML content and by operating systems to associate file types with the proper applications. If you’ve ever double-clicked on a document directly and had your word processor open, you’ve seen MIME in action.
The MIME standard is defined in several RFC publications, but for this discussion, the primary references are RFC 2045, which defines the Content-Type header, and RFC 2046, which defines valid media types that can appear there.
MIME types are defined for common file formats to allow programs like browsers and email clients to handle the attachments properly. An email client may opt to display image-typed attachments directly or use the MIME type to open a companion application like a spreadsheet or word processor.
MIME types consist of a main type, such as image, text, or audio, and a subtype. Both parts are listed together as maintype/subtype, so HTML files are of the text/html type, while JPEG images are image/jpeg. Separate the two parts when using .add_attachment().
There are hundreds of predefined MIME types for common file types. You can find MIME type lists online, such as in the Mozilla Developer Guide. If you don’t know the MIME type for an attachment, searching for the file suffix and “MIME type” will often yield positive results. However, you can always drop back to the catch-all type application/octet-stream, which is used for arbitrary binary data.
This results in the following email being sent:
1---------- MESSAGE FOLLOWS ----------
2to: your@gmail.com
3from: my@gmail.com
4subject: Attachment Test Message
5MIME-Version: 1.0
6Content-Type: multipart/mixed; boundary="===============6673542823507367099=="
7X-Peer: ('::1', 59526, 0, 0)
8
9--===============6673542823507367099==
10Content-Type: text/plain; charset="utf-8"
11Content-Transfer-Encoding: 7bit
12
13Please find a JPG attached.
14
15--===============6673542823507367099==
16Content-Type: image/jpeg
17Content-Transfer-Encoding: base64
18Content-Disposition: attachment; filename="smiley-small.jpg"
19MIME-Version: 1.0
20
21/9j/4AAQSkZJRgABAQEAYABgAAD/4QBARXhpZgAASUkqAAgAAAABAGmHBAABAAAAGgAAAAAAAAAC
22AAKgCQABAAAAwAAAAAOgCQABAAAAkAAAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH
23... <more data here> ...
24iQfwooUfkKsrbqvSv0DKfCvB4eSqZlVdV9l7sfnq2/vXoeVXzypNWox5fxZz2g+FLLQ4RHbQCPP3
25m6s31Nb8UIXoKlCAU6v2nC4OhgqUaOHgoxWySskfOzqSqScpO7EAxS0UV2mQUUUUAFFFFAH/2Q==
26
27--===============6673542823507367099==
28------------ END MESSAGE ------------
In this case, the email Content-Type is set to multipart/mixed, indicating that the email contains multiple parts of mixed types, such as a text message and an attachment. The boundary specified separates each part from the others:
- The boundary on line 9 separates the headers from the text part of the message.
- The boundary on line 15 separates the first (and only) attachment.
- The final boundary on line 27 marks the end of the message.
These boundary properties pop up whenever you send an email with more than just a simple text message.
Note: How does the data from the JPEG image get turned into the text you see? The answer is in the Content-Transfer-Encoding header on line 17, which lists the scheme as base64.
The base64 encoding scheme breaks binary data into consecutive sequences of 6 bits, each of which maps to one of 64 printable characters. The resulting text can be transferred via email or embedded in an HTML document and decoded on the receiving end. It’s widely used not only for email but also for sending binary objects in HTML.
You can use this technique to attach any number of items to your emails.
Adding Alternate reply-to Addresses
Sometimes, you want replies to your emails to go to a different account. Perhaps you’re sending automated emails from a developer account but want replies to be directed to a support account or another monitored mailbox. SMTP and EmailMessage have you covered there as well, using the same mechanism you saw previously:
reply_to_email.py
1from send_msg import send
2from email.message import EmailMessage
3
4sender_email = "my@gmail.com"
5reply_email = "my.different@gmail.com"
6receiver_email = "your@gmail.com"
7
8# Build Email Message
9msg = EmailMessage()
10msg["to"] = receiver_email
11msg["from"] = sender_email
12msg["reply-to"] = reply_email
13msg["subject"] = "Reply Please"
14msg.set_content("Replies go to a different mailbox.")
15
16# Send message
17send(msg, sender_email)
This results in the following email:
---------- MESSAGE FOLLOWS ----------
to: your@gmail.com
from: my@gmail.com
reply-to: my.different@gmail.com
subject: Reply Please
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 49554, 0, 0)
Replies go to a different mailbox.
------------ END MESSAGE ------------
The reply-to header is defined in the SMTP standard. Any valid SMTP header can be specified in this way in your email messages. Some common SMTP headers you may see and want to be aware of include:
return-path: An email address for error messages or bounced emails.message-id: A unique email identifier to avoid duplication and enable threading.in-reply-to: A reference to a message you’re replying to.content-language: The language the email is written in.
In addition, any headers that start with x- or X- are custom headers called X-headers. These are defined by email clients and servers for specific purposes, such as email tracking, metrics reporting, and compliance.
Sending Email to Multiple Recipients
Of course, you often need to send emails to several people using the To, CC, or BCC lines. The EmailMessage class enables this functionality.
As with the To field, the CC and BCC fields are dictionary key names:
multiple_recipients.py
1from send_msg import send
2from email.message import EmailMessage
3
4sender_email = "my@gmail.com"
5receiver_email_1 = "your@gmail.com"
6receiver_email_2 = "your_other@gmail.com"
7cc_receiver_email = "cc-you@gmail.com"
8bcc_receiver_email = "bcc-you@gmail.com"
9
10# Build Email Message
11msg = EmailMessage()
12msg["to"] = [receiver_email_1, receiver_email_2]
13msg["cc"] = cc_receiver_email
14msg["bcc"] = bcc_receiver_email
15msg["from"] = sender_email
16msg["subject"] = "Test Message"
17msg.set_content("This is a test message")
18
19# Send message
20send(msg, sender_email)
You can specify multiple recipients by adding them to any iterator, such as a tuple or list, as shown on line 12.
Here’s the output of this operation:
---------- MESSAGE FOLLOWS ----------
to: your@gmail.com, your_other@gmail.com
cc: cc-you@gmail.com
from: my@gmail.com
subject: Test Message
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 44464, 0, 0)
This is a test message
------------ END MESSAGE ------------
Wait, where’s the bcc? It’s not shown here, as it’s never listed as a message header. The email addresses in the BCC list are sent to the server as message recipients only. For more information on how blind copies are handled, consult the SMTP RFC 5321, Section 7.2.
As you know, listing all the email recipients in your code is time-consuming and inefficient, especially if you have many people to include. You may also want to personalize the email with recipients’ names or other information.
A good starting point for sending multiple personalized emails is to create a CSV (comma-separated values) file that contains all the required information. A CSV file can be thought of as a simple table, where the first line often contains the column headers.
Below are the contents of the file contacts.csv, which is saved in the same folder as the example Python code. It contains the names, email addresses, and grades for a set of fictional students. The email addresses use the my+modifier@gmail.com construction to make sure all emails end up in the same inbox, which in this example is <my@gmail.com>:
contacts.csv
name,email,grade
Ron Obvious,my+obvious@gmail.com,B+
Killer Rabbit,my+rabbit@gmail.com,A
Brian,my+brian@gmail.com,C
When creating a CSV file, make sure to separate your values with a comma and without any surrounding whitespace.
The code example below opens this CSV file and loops over its contents, sending an email to each contact with their name and grade:
csv_email.py
1import csv
2from email.message import EmailMessage
3
4from send_msg import send
5
6sender_email = "my@gmail.com"
7
8with open("contacts.csv") as file:
9 reader = csv.reader(file)
10 next(reader) # Skip header row
11 for name, email, grade in reader:
12 msg = EmailMessage()
13 msg["to"] = f"{name} <{email}>"
14 msg["from"] = f"Me <{sender_email}>"
15 msg["Subject"] = "Your grade"
16 msg.set_content(f"Congratulations, {name}, you got a {grade}.")
17
18 send(msg, sender_email)
Notice lines 13 and 14, which set the to and from accounts as display_name <email>. This pattern conforms to the RFC standard and is typically used when sending email from a known contact.
Here’s what those emails look like:
---------- MESSAGE FOLLOWS ----------
to: Ron Obvious <my+obvious@gmail.com>
from: Me <my@gmail.com>
Subject: Your grade
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 58674, 0, 0)
Congratulations, Ron Obvious, you got a B+.
------------ END MESSAGE ------------
---------- MESSAGE FOLLOWS ----------
to: Killer Rabbit <my+rabbit@gmail.com>
from: Me <my@gmail.com>
Subject: Your grade
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 39990, 0, 0)
Congratulations, Killer Rabbit, you got a A.
------------ END MESSAGE ------------
---------- MESSAGE FOLLOWS ----------
to: Brian <my+brian@gmail.com>
from: Me <my@gmail.com>
Subject: Your grade
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 45298, 0, 0)
Congratulations, Brian, you got a C.
------------ END MESSAGE ------------
You’ll notice that there are three separate messages listed, which is what you expect.
Of course, you can parse and use the CSV file directly, but the email package has another trick up its sleeve called the Address class.
Using the Address Class
You can create instances of the Address class to hold the display name and email address associated with an email contact. You can specify the email address in two ways:
- As a single full email address
- As a two-part address consisting of the username and the domain
In the first case, you create the Address object using the addr_spec parameter. In the second, you use the username and domain parameters, as shown below:
spam = Address(display_name="Spam", addr_spec="spam@gmail.com")
eggs = Address(display_name="Eggs", username="eggs", domain="gmail.com")
Here’s the same CSV example, but using Address objects:
address_email.py
1import csv
2from email.message import EmailMessage
3from email.headerregistry import Address
4
5from send_msg import send
6
7sender_email = "my@gmail.com"
8sender = Address(display_name="Me", addr_spec=sender_email)
9
10with open("contacts.csv") as file:
11 reader = csv.reader(file)
12 next(reader) # Skip header row
13 for name, email, grade in reader:
14 recipient = Address(display_name=name, addr_spec=email)
15 msg = EmailMessage()
16 msg["to"] = recipient
17 msg["from"] = sender
18 msg["Subject"] = "Your grade"
19 msg.set_content(f"Congratulations, {name}, you got a {grade}.")
20
21 send(msg, sender_email)
You construct an Address object for your sender email on line 8 and one for each of the contacts in contacts.csv on line 14. The EmailMessage class pulls the data out of the Address objects when assembling the message. Running this example results in the same three emails as before:
---------- MESSAGE FOLLOWS ----------
to: Ron Obvious <my+obvious@gmail.com>
from: Me <my@gmail.com>
Subject: Your grade
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 49858, 0, 0)
Congratulations, Ron Obvious, you got a B+.
------------ END MESSAGE ------------
---------- MESSAGE FOLLOWS ----------
to: Killer Rabbit <my+rabbit@gmail.com>
from: Me <my@gmail.com>
Subject: Your grade
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 40574, 0, 0)
Congratulations, Killer Rabbit, you got a A.
------------ END MESSAGE ------------
---------- MESSAGE FOLLOWS ----------
to: Brian <my+brian@gmail.com>
from: Me <my@gmail.com>
Subject: Your grade
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 53752, 0, 0)
Congratulations, Brian, you got a C.
------------ END MESSAGE ------------
Each email still shows to and from accounts as display_name <email>. However, you didn’t have to construct those strings yourself—the .__str__() method of the Address class produces them automatically.
Using Third-Party Email Libraries
There are a few libraries designed to make sending email in Python easier. While you should find one that works for you, this tutorial will mention two of them. Yagmail can be used for Gmail, and the Proton Mail Bridge client is used exclusively with Proton Mail.
Using Yagmail
Yagmail simplifies connecting to Gmail for accounts that use OAuth2. However, it’s a Gmail-specific library, and OAuth2 is complex and requires frequent re-authorization to remain viable. This library was last updated in 2022, so it may not include recent changes made by Google.
That said, Yagmail is well documented, including the OAuth2 setup. If you’re using Gmail and prefer OAuth2 over app passwords, Yagmail can be very useful.
You can install Yagmail using pip:
$ python -m pip install "yagmail[all]"
The [all] ensures the keyring library is included, which allows you to securely store your Gmail username and app password using the system keyring service. You can read more about how that works in the Yagmail documentation on configuring your credentials.
After installing and configuring Yagmail, you can send an email with a PDF attachment in a fraction of the lines needed for the earlier examples:
yagmail_example.py
import yagmail
receiver = "your@gmail.com"
body = "Hello there from Yagmail"
filename = "document.pdf"
yag = yagmail.SMTP("my@gmail.com")
yag.send(
to=receiver,
subject="Yagmail test with attachment",
contents=[body, filename],
)
Note that if you don’t configure Yagmail to use either the keyring or OAuth2, it will prompt you to enter your password when connecting and will automatically save it in the keyring.
Using the Proton Mail Bridge Client
Using the Proton Mail Bridge client first requires that you have the Proton Mail Bridge installed and working on your machine. This only works for paid Proton Mail accounts, so keep that in mind. The Bridge also needs to be up and running before your Python code runs, which can complicate your workflow.
As always, you need to install the library before you can use it:
$ python -m pip install proton-mail-bridge-client
After installing the library, you can send an email in the following way:
proton_example.py
from proton_mail_bridge_client import ProtonMailClient
with ProtonMailClient(
email="your-email@proton.me",
password="your-bridge-password" # Bridge password, NOT account password
) as client:
client.send_mail(
to="you@proton.com",
subject="Test",
body="This is a test",
)
The library also allows you to read and manage your Proton Mail account using the Proton Mail Bridge.
Using Transactional Email Services
Transactional email services are worth considering if you need to send email at high volumes or on a regular schedule, track delivery statistics, or ensure reliable delivery.
Most of these services offer a free plan or trial so you can try them out, with paid upgrades available when you need higher volume. Some of the free plans have no time limit and may be enough on their own.
Below is an overview of the plans and trials offered by certain transactional email services. This information is accurate as of April 2026 and may change over time.
First, some providers offer free plans with limited functionality and sending caps. These plans don’t have time limits:
| Provider | Plan Details |
|---|---|
| Brevo | 300 emails/day |
| Mailgun | 100 emails/day |
| Mailjet | 200 emails/day, 6,000 emails/month |
| Mailchimp | 500 emails/month, 250 contacts |
The following providers offer free trials that are time-limited and designed to be upgraded to paid plans when they expire:
| Provider | Plan Details |
|---|---|
| SendGrid | 100 emails/day for 60 days |
| Amazon SES | 3,000 emails/month for the first 12 months |
There are, of course, many other email services available, and you should always search for the best solution for your particular use case.
Several of these services provide official Python libraries. Here’s a quick example using SendGrid’s. To run this code, follow these steps:
- Sign up for a free SendGrid account.
- Request an API key for authentication.
- Store the API key in your environment using the appropriate command:
- Linux/macOS shell:
export SENDGRID_API_KEY='YOUR_API_KEY' - Windows Command Prompt:
set SENDGRID_API_KEY=YOUR_API_KEY - Windows PowerShell:
$env:SENDGRID_API_KEY = "YOUR_API_KEY"
- Linux/macOS shell:
- Install the library in your Python environment with
python -m pip install sendgrid.
This will send a single email and print status information:
sendgrid_example.py
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
message = Mail(
from_email="from_email@example.com",
to_emails="to@example.com",
subject="Sending with Twilio SendGrid is Fun",
html_content="<strong>and easy to do anywhere, even with Python</strong>",
)
try:
sg = SendGridAPIClient(os.environ.get("SENDGRID_API_KEY"))
response = sg.send(message)
print(response.status_code)
print(response.body)
print(response.headers)
except Exception as e:
print(e.message)
The complete documentation for the library can be found in the sendgrid-python repository.
Conclusion
You made it to the end of the tutorial! Whether you came here to learn how to send confirmation emails to users, enable support emails directly from your code, or send reminders to everyone on your email list, you now have the tools to do it.
In this tutorial, you learned how to:
- Set up and configure new email services for access from Python.
- Configure secure connections using
.SMTP_SSL(). - Use the
emailandsmtpliblibraries to send emails with text and HTML content, attachments, and personalization. - Manage email headers to set alternate
reply-toaddresses. - Use transactional email services to send mass emails.
If you prefer video, Real Python’s Sending Emails Using Python course follows the same path and finishes by building a CSV-powered script. Enjoy sending emails with Python!
Get Your Code: Click here to download the free sample code you’ll use to learn how to send plain-text and HTML emails, attach files, and automate email delivery with Python.
Frequently Asked Questions
Now that you have some experience with sending emails in Python, you can use the questions and answers below to check your understanding and recap what you’ve learned.
These FAQs are related to the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.
Python’s standard library includes the smtplib module to connect to an SMTP server and the email package to build the message. Together, they let you send mail from any account that allows SMTP access, without needing third-party libraries.
Sending email with Python is safe when you use an encrypted connection. The smtplib.SMTP_SSL() constructor paired with ssl.create_default_context() validates the server’s certificate and protects your credentials and message content in transit.
Create a dedicated Gmail account, turn on two-factor authentication, and generate an app password from your Google account settings. Then connect to smtp.gmail.com on port 465 with smtplib.SMTP_SSL() and log in with your email address and the app password.
Yes. The EmailMessage class provides an .add_attachment() method that reads binary file data and accepts a MIME type, then handles the base64 encoding automatically so images, PDFs, and other files can be included with the message.
For small contact lists, looping over a CSV file and building a personalized EmailMessage for each recipient works well with the standard library. For higher volumes, transactional services like SendGrid, Mailgun, or Brevo handle deliverability, statistics, and rate limiting that smtplib alone doesn’t provide.
Take the Quiz: Test your knowledge with our interactive “Sending Emails With Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Sending Emails With PythonUse Python's standard library to send email through secure SMTP connections, attach files, include HTML content, and route replies.





