Python can send email with nothing but the standard library: smtplib talks to the mail server, email.message.EmailMessage builds the message. Ten lines of code are enough for a plain-text message. The part that takes the time is everything around it: TLS, Gmail passwords that suddenly stop working, attachments, and cloud hosts that block your port.
This guide covers the full path, from a first message sent to a local test server to HTML, attachments, Gmail, error handling, and the point where an email API becomes the better tool.
How sending email from Python works
SMTP (Simple Mail Transfer Protocol) is the protocol that moves a message from a sender to a mail server. When you call smtplib, your script behaves like a mail client: it opens a connection to an SMTP server, authenticates, hands over the message and disconnects. The server then delivers it onward
That has two consequences worth knowing before you write any code.
First, you need an SMTP server that accepts you. That can be Gmail, your company's mail server, a hosting provider's relay, or a transactional email service. Python does not ship with one that delivers to the internet.
Second, building the message and sending it are separate steps. EmailMessage creates headers and body. smtplib transports it. Mixing the two up is the source of most old tutorials' ugly code (manual MIMEText assembly, hand-written header strings). Since Python 3.6 the EmailMessage API is the one to use.
Send your first email without a real server
Test locally before you touch real credentials. The aiosmtpd package runs a throwaway SMTP server that prints every message it receives. (The old python -m smtpd debug server was removed in Python 3.12, so aiosmtpd is the replacement.)
The first terminal prints the full message with its headers. Nothing leaves your machine, so you can iterate on subject lines, recipients and formatting without sending real mail.
Send email through a real SMTP server
A real server adds two requirements: an encrypted connection and a login. SMTP has two common ways to encrypt.
Port 465 version:
Port 587 version:
Two details matter here. Pass an explicit ssl.create_default_context() so the server certificate is actually verified. And read credentials from environment variables, not from the source file. A password committed to a repository is the most common way these scripts leak.
Send email from Gmail with Python
Gmail uses smtp.gmail.com, port 465 for SSL or 587 for STARTTLS. The code is the same as above. What trips people up is the password.
Google removed "less secure app" access, so logging in with your regular account password fails. The working setup:
- Turn on 2-Step Verification for the Google account.
- Create an app password in the account's security settings. It is a 16-character code made for one app.
- Use that code as SMTP_PASSWORD, with your full Gmail address as SMTP_USER.
Gmail is fine for personal scripts and alerts to yourself. It enforces daily sending limits and rewrites the sender to the logged-in account, so it is a poor base for anything customer-facing.
Send HTML email and multiple recipients
For an HTML message, set a plain-text body first and add the HTML as an alternative. Clients that cannot render HTML, and spam filters that expect a text part, both use the plain version.
send_message() reads To, Cc and Bcc and delivers to all of them. It also strips the Bcc header from the copy that goes out, so the blind recipients stay hidden. To build the header from a list, use ", ".join(addresses).
Send an email with an attachment
EmailMessage.add_attachment() takes the file as bytes, plus a MIME type and a filename. Use mimetypes to pick the type instead of hard-coding it:
The same code attaches a script. A file named job.py is guessed as text/x-python, and the recipient gets it as a normal attachment. Some corporate gateways block .py and other executable-looking extensions, so for those recipients zip the file first.
Keep an eye on size. Most servers refuse messages above roughly 20 to 25 MB once the attachment is base64-encoded, which inflates it by about a third. For big files, send a link instead.
Fix the errors you will actually hit
When a connection misbehaves, server.set_debuglevel(1) prints the whole SMTP conversation. It shows which step fails: connect, TLS, login, or recipient.
SMTP or an email API: which one to use
SMTP is the right choice when you already have a server to relay through, when the volume is small, or when you want zero dependencies. It starts to hurt in a few predictable cases:
- You send from your own domain and need SPF, DKIM and DMARC set up so mail does not land in spam.
- Your host blocks SMTP ports.
- You need delivery events, bounces or open tracking.
- You need to send in volume, where connection handling and retries become your problem.
Email APIs (SendGrid, Mailgun, Resend, Amazon SES and others) take the same message over HTTPS. The shape is nearly identical across providers, here with requests:
Port 443 is rarely blocked, there is no TLS negotiation to get wrong, and errors come back as JSON with a status code. The trade-off is a new dependency, an account, and usually a verified domain. Check each provider's docs for the exact endpoint and field names. They are not interchangeable.
What SMTP cannot do: receive and read email
Everything above is one-way. smtplib sends. It cannot list an inbox, read a reply or search a thread. In Python, the built-in route for reading is imaplib, which speaks IMAP. It works, but it is a verbose, stateful protocol, and Gmail and most providers require OAuth or app passwords for it as well.
For scripts that only notify, one-way is fine. For scripts that wait for an answer (confirm a code, handle a reply, continue a conversation), you need a second channel or a service that gives you both directions.
Sending and receiving from a Python AI agent
This is the case where the SMTP-plus-IMAP approach gets awkward. An autonomous agent that must sign up for a service, receive a verification email and answer a reply should not use your personal Gmail login. It needs an inbox of its own, and ideally one it can create without a human filling in a form.
Atomic Mail Agentic is built for that. An agent registers its own @atomicmail.ai inbox through a proof-of-work challenge, with no domain, card or phone verification. The inbox API is JMAP, the JSON email protocol standardized in RFC 8620 and RFC 8621, which covers both sending and reading in one request format. The service is in open alpha and free. Errors come back with a plain-language hint, which suits agents that debug their own requests.
From Python, the shortest path is the AgentSkill CLI. Register once:
The credentials live in ~/.atomicmail, so the script never handles a password. The same CLI reads the inbox, so the agent can pick up the reply to the message it just sent. If you would rather skip the CLI and call JMAP over HTTPS directly from requests, the Atomic Mail docs walk through the session flow, and the guide to building an AI email agent with its own inbox shows a full send, receive and reply loop.
FAQ
Is there an SMTP library in Python?
Yes. smtplib is in the standard library, so there is nothing to install. It pairs with email.message.EmailMessage for building the message.
How to send email in Python without SMTP?
Call an email API over HTTPS with requests or the provider's SDK. SendGrid, Mailgun, Resend and Amazon SES all offer one. For an agent that must also receive mail, use an API with an inbox, such as JMAP.
How to send email using SMTP?
Connect to the server, encrypt the connection, log in, then send. In Python: SMTP_SSL(host, 465) or SMTP(host, 587) followed by starttls(), then login() and send_message().
How do I email a .py file?
Attach it with add_attachment() using maintype="text" and subtype="x-python", or let mimetypes.guess_type() choose. Zip it if the recipient's mail system blocks script files.
Why does Gmail return SMTPAuthenticationError 535?
You are using your account password. Gmail only accepts an app password over SMTP, and creating one requires 2-Step Verification.
Which SMTP port should I use: 25, 465 or 587?
Use 587 with STARTTLS or 465 with implicit TLS. Port 25 is for server-to-server relay, and many hosts block it for outbound traffic.
Can Python receive and read email too?
Yes, with imaplib for IMAP, or through an API that exposes an inbox. smtplib alone only sends.
Should I hard-code my SMTP password?
No. Read it from an environment variable or a secrets manager. A password in source ends up in version control sooner or later.
What is the difference between send_message() and sendmail()?
send_message() takes an EmailMessage and reads the sender and recipients from its headers. sendmail() takes raw strings and an explicit envelope, which you only need for unusual routing. Prefer send_message().
Which route to pick
If you send occasional mail from a script you own, smtplib with an app password or your provider's SMTP credentials is enough. If you send from your own domain or at volume, use an email API. If the script is an agent that has to receive and answer mail on its own, it needs an inbox it owns, not a borrowed Gmail login.
.png)