Guide
Send Email from Python: SMTP, API, and CLI
Python has three practical paths for sending email in 2026: the built-in smtplib module over SMTP, the Gmail API through Google's OAuth 2.0 client libraries, and a CLI subprocess call that skips both protocols entirely. This guide shows working code for each and helps you pick the right one.
Written by Qasim Muhammad Staff SRE
Command references used in this guide: nylas email send for the subprocess pattern, nylas auth config for headless API-key setup, nylas auth login for OAuth authentication, and nylas email list for reading email.
What are the 3 ways to send email from Python?
Python can send email through three distinct paths: the standard library's smtplib module (SMTP protocol), a provider SDK like Google's google-api-python-client (REST API with OAuth 2.0), or a subprocess.run call to an external CLI (API-based, no protocol code). Each trades setup complexity for flexibility. The table below shows the tradeoffs before you write any code.
| Method | Setup time | Auth | Providers | Lines of code |
|---|---|---|---|---|
| smtplib | ~5 min | App password | 1 (per config) | ~15 |
| Gmail API | ~20 min | OAuth 2.0 | Gmail only | ~40 |
| CLI subprocess | ~2 min | API key / OAuth | 6 (Gmail, Outlook, Exchange, Yahoo, iCloud, IMAP) | ~8 |
The right choice depends on your constraints. A throwaway data-pipeline script that only touches Gmail can get away with smtplib and an app password. A production Gmail integration that needs OAuth token refresh and audit trails should use the Gmail API. A multi-provider script, CI job, or AI agent that needs to send from any mailbox without protocol code can shell out to a CLI.
How do I send email with Python smtplib?
Python's smtplib connects directly to an SMTP server and delivers a message using the SMTP protocol. It ships with the standard library, so there's nothing to install. For Gmail, you connect to smtp.gmail.com on port 587 with STARTTLS. Since Google removed "less secure app" access on September 30, 2024, you need a 16-character app password generated from your Google Account security settings with 2-Step Verification enabled.
The code below sends a plain-text email in about 15 lines. It uses Python 3.6+'s EmailMessage class, which handles MIME encoding automatically. The starttls() call upgrades the connection to TLS before sending credentials, which prevents the password from traveling in cleartext.
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Daily import finished"
msg["From"] = "you@gmail.com"
msg["To"] = "ops@example.com"
msg.set_content("The 2026-05-21 import completed successfully.")
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.ehlo()
server.starttls()
server.ehlo()
server.login("you@gmail.com", "xxxx xxxx xxxx xxxx") # app password
server.send_message(msg)
print("sent")If you skip the app password and try a regular Google password, Gmail rejects the connection with SMTPAuthenticationError(535, b'5.7.8 Username and Password not accepted'). Outlook has a similar requirement since Microsoft retired Basic Auth for Exchange Online in October 2022. Each provider has its own SMTP host, port, and credential rules, so switching providers means rewriting the connection block.
How do I send email with the Gmail API in Python?
The Gmail API replaces SMTP with a REST endpoint and OAuth 2.0 tokens. It's the right path for production Gmail integrations because tokens refresh automatically, you get fine-grained scopes, and there's no SMTP credential to rotate. The tradeoff: you need a Google Cloud project, an OAuth consent screen, a credentials.json download, and three pip packages (google-api-python-client, google-auth-httplib2, google-auth-oauthlib) before you send your first email.
According to Google's quota documentation, each messages.send call costs 100 quota units, and the per-user limit is 250 units per second. That math caps a single user at roughly 2 sends per second before hitting rate limits. The OAuth setup takes 15-20 minutes the first time through the Google Cloud Console.
import base64
import os.path
from email.message import EmailMessage
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
def get_gmail_service():
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as token:
token.write(creds.to_json())
return build("gmail", "v1", credentials=creds)
def send_gmail(to: str, subject: str, body: str) -> dict:
service = get_gmail_service()
msg = EmailMessage()
msg["To"] = to
msg["Subject"] = subject
msg.set_content(body)
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
return (
service.users()
.messages()
.send(userId="me", body={"raw": raw})
.execute()
)
send_gmail(
"ops@example.com",
"Daily import finished",
"The 2026-05-21 import completed successfully.",
)That's about 40 lines of code. The get_gmail_service() function handles the full OAuth lifecycle: first-run consent, token caching in token.json, and automatic refresh when the access token expires (every 3,600 seconds). The actual send is a base64-encoded MIME message posted to the messages.send endpoint. This approach is Gmail-only. To add Outlook, you'd need a separate Microsoft Graph integration with MSAL and a different OAuth flow.
How do I send email from Python without SMTP?
The subprocess pattern calls an external CLI binary from Python, passing recipient, subject, and body as arguments. No SMTP connection, no OAuth boilerplate, no provider SDK. The CLI handles authentication, token refresh, and delivery over HTTPS. One function works with Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP accounts. The code below sends an email in 8 lines.
This approach is covered in depth in the Send Email from Python Without SMTP guide, which includes error handling, bulk CSV sends, and Agent Account patterns. Here's the core function.
import json
import subprocess
def send_email(to: str, subject: str, body: str) -> dict:
result = subprocess.run(
["nylas", "email", "send",
"--to", to,
"--subject", subject,
"--body", body,
"--yes", "--json"],
check=True, capture_output=True, text=True, timeout=30,
)
return json.loads(result.stdout)
send_email(
"ops@example.com",
"Daily import finished",
"The 2026-05-21 import completed successfully.",
)The --yes flag skips the interactive confirmation prompt so the script runs unattended. The --json flag returns structured output with the message ID, thread ID, and send timestamp. The timeout=30 catches stalled connections instead of hanging indefinitely. Unlike the Gmail API path, switching from Gmail to Outlook doesn't require changing any Python code because the provider is configured in the CLI, not the script.
Which method should I use?
The decision depends on four factors: how many providers you need, whether credentials are acceptable in your environment, how much code you want to own, and whether the script runs interactively or in automation. The table below maps common use cases to the best method and explains why each wins for that scenario.
| Use case | Best method | Why |
|---|---|---|
| Quick one-off script | smtplib | Zero dependencies, ships with Python, 15 lines |
| Gmail-only production app | Gmail API | OAuth tokens, no app passwords, fine-grained scopes |
| Multi-provider automation | CLI subprocess | One function for 6 providers, no protocol code |
| CI/CD pipeline | CLI subprocess | No browser for OAuth, works in containers where SMTP ports are blocked |
| AI agent email tool | CLI subprocess or MCP | JSON output, non-interactive, --yes skips prompts |
| Cron job alerts | smtplib or CLI | smtplib if you already have app passwords configured; CLI if ports 25/465/587 are blocked |
A few edge cases worth noting. If you need to send attachments, smtplib requires manual MIME multipart construction (roughly 30 extra lines). The Gmail API accepts base64-encoded attachments up to 25 MB. The CLI takes a --attach flag on the draft-then-send path. If you need to read email too, smtplib can't help because it's send-only; you'd need imaplib or the Gmail API for that. The CLI handles both with nylas email list and nylas email send.
What errors will I hit?
Each method has its own failure mode. Knowing the error before you hit it saves debugging time. The three most common failures across all three methods are authentication rejection, timeout on stalled connections, and rate-limit throttling. Here's what to expect from each.
smtplib: SMTPAuthenticationError(535, b'5.7.8 Username and Password not accepted') means you're using a regular password instead of an app password, or 2-Step Verification isn't enabled. SMTPServerDisconnected usually means the wrong port (use 587 for STARTTLS, 465 for SSL). Gmail throttles to about 500 recipients per day for consumer accounts.
Gmail API: HttpError 403 "insufficientPermissions" means your OAuth scope doesn't include gmail.send. HttpError 429 means you've exceeded the 250 quota units per second per user limit. Delete token.json and re-authenticate if you change scopes after the initial consent.
CLI subprocess: CalledProcessError with exit code 1 and stderr containing not authenticated means you haven't run nylas auth config or nylas auth login on the host. TimeoutExpired fires after 30 seconds if the API endpoint is unreachable.
Next steps
- Send email from Python without SMTP -- deep-dive on the subprocess pattern with error handling, CSV bulk sends, and Agent Accounts
- Send email from the terminal -- the base CLI send workflow for Linux, macOS, and Windows
- Getting started with Nylas CLI -- install and authenticate in under 2 minutes
- AI agent CLI for email and calendar -- subprocess patterns for custom AI agents
- GitHub Actions email notifications -- CI/CD pipeline email from Python and bash
- nylas email send command reference -- every flag for recipients, scheduling, GPG, and JSON output
- Full command reference -- every flag and subcommand documented