Security note
Don’t hardcode passwords.
Prefer:
- app passwords
- environment variables
- secret managers
Plain text email
import os
import smtplib
from email.message import EmailMessage
SMTP_HOST = "smtp.gmail.com"
SMTP_PORT = 587
def send_email(to: str, subject: str, body: str):
user = os.environ["SMTP_USER"]
password = os.environ["SMTP_PASS"]
msg = EmailMessage()
msg["From"] = user
msg["To"] = to
msg["Subject"] = subject
msg.set_content(body)
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
smtp.starttls()
smtp.login(user, password)
smtp.send_message(msg)
# send_email("to@example.com", "Hello", "Test")Visualize it
Sending an email with smtplib follows a straightforward compose-connect-send sequence.
flowchart LR
A["Compose message (subject, body, attachments)"] --> B["Connect to SMTP server (TLS)"]
B --> C["Log in"]
C --> D["Send"]
D --> E["Close connection"]
How the pieces fit
Sending email from Python uses two standard-library modules.
email.message.EmailMessagebuilds the message: headers (From,To,Subject) and the body. It handles encoding for you, including non-ASCII characters in the subject and body.smtplibdelivers the message by talking to a mail server using SMTP.
The steps are always the same: connect to the server, upgrade to an encrypted connection, log in, send, and disconnect. The with statement closes the connection for you.
Ports and encryption
- Port 587 starts as plain text and upgrades with
starttls(). This is what the example uses. - Port 465 is encrypted from the start. Use
smtplib.SMTP_SSL(host, 465)and skipstarttls(). - Port 25 is for server-to-server mail. Most providers block it for clients.
Never send a password over an unencrypted connection. Always call starttls() before login().
Passwords and Gmail
Do not put your password in the script. Read it from an environment variable, as the example does. Set them in your shell (export SMTP_PASS=... on Linux and macOS, setx SMTP_PASS "..." on Windows), or load them from a .env file that is listed in .gitignore.
Gmail does not accept your normal account password for scripts. Turn on 2-step verification, create an app password, and use that. Other providers, such as Outlook.com, SendGrid, and Mailgun, have their own SMTP settings, and many now require OAuth or an API key instead. Check the provider's documentation for the host, port, and authentication method.
A sender you can test
This version separates building the message from sending it. You can check the message without a server, and send to several people.
import os
import smtplib
from email.message import EmailMessage
def build_message(sender: str, to: list[str], subject: str, body: str) -> EmailMessage:
msg = EmailMessage()
msg["From"] = sender
msg["To"] = ", ".join(to)
msg["Subject"] = subject
msg.set_content(body)
return msg
def send(msg: EmailMessage, dry_run: bool = True) -> None:
if dry_run:
print(msg) # shows headers and body, sends nothing
return
with smtplib.SMTP("smtp.gmail.com", 587, timeout=20) as smtp:
smtp.starttls()
smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
smtp.send_message(msg)
message = build_message("me@example.com", ["you@example.com"], "Hello", "Test message")
send(message, dry_run=True)dry_run=True is the default, so a mistake never sends mail. Turn it off only after you have read the printed message. Pass timeout so a stalled server does not hang the script.
Safety
Email cannot be recalled. Test with your own address first. If a loop builds messages for many people, limit it while testing, and send at a modest speed: providers cap daily volume and will suspend an account that looks like spam. Do not send bulk unsolicited mail.
Common mistakes
- Hardcoding the password.
- Forgetting
starttls(), so login fails or sends the password in clear text. - Using the account password with Gmail instead of an app password.
- Building headers by string concatenation. A newline in user-supplied text can inject extra headers.
EmailMessagerejects that, so use it. - No timeout.
- No error handling. Catch
smtplib.SMTPAuthenticationErrorandsmtplib.SMTPException, and log what failed.
🧪 Try It Yourself
Exercise 1 – List Files with os.listdir
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 2 – Join Paths with os.path.join
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 3 – Write and Read a File
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.