Sending emails manually is fine when you send one or two. But what about when you need to send daily reports to your team? Or get alerted when your web scraper finds something? Or email yourself when a backup finishes?

That's where Python email automation comes in. In this guide, you'll go from sending your first automated email to building a scheduled email notification system — all in Python, no third-party services required.

If you've read our Python Web Scraping guide, imagine combining the two: scrape product prices every morning, and email yourself a price drop report. That's exactly the kind of project you'll be able to build by the end.

Why Python for Email?

You might wonder — why not just use Gmail's scheduled send?

Because automation means triggered by events, not time alone. Python lets you send emails when: - A web scraper finds a price drop - A backup script fails - Your server runs low on disk space - A new file appears in a folder

Email becomes a consequence of something happening, not a manual action.

Plus, Python's smtplib is built into the standard library — no pip install required.

Setup: Gmail App Password

Before writing any code, you need an App Password. You can't use your regular Gmail password with automated scripts — Google blocks "less secure apps."

Here's how to get one (2 minutes):

  1. Go to your Google Account Security page
  2. Enable 2-Step Verification if you haven't already
  3. Search for "App Passwords" in the Google account settings
  4. Select "Mail" as the app and "Other" as the device, name it "Python Script"
  5. Copy the 16-character password that appears (with spaces is fine)

Important: You'll only see this password once. Save it somewhere safe — we'll use it as an environment variable so it never appears in your code:

export GMAIL_APP_PASSWORD="xxxx xxxx xxxx xxxx"

Alternatively, use a dedicated "bot" Gmail account for automation — it's cleaner and keeps your personal inbox separate.

Your First Automated Email in 10 Lines

Here's the simplest possible email script:

import smtplib
from email.mime.text import MIMEText

# Your credentials
SENDER = "yourname@gmail.com"
PASSWORD = "xxxx xxxx xxxx xxxx"  # App Password, not your regular password
RECEIVER = "yourname@gmail.com"

# Create the message
msg = MIMEText("Hey! This email was sent by a Python script.")
msg["Subject"] = "My First Automated Email"
msg["From"] = SENDER
msg["To"] = RECEIVER

# Send it
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
    server.login(SENDER, PASSWORD)
    server.send_message(msg)

print("Email sent!")

Run it, check your inbox. If it worked, you'll see the email. If you get an authentication error, double-check your App Password.

What's happening: 1. MIMEText creates a plain-text email 2. SMTP_SSL opens an encrypted connection to Gmail's server on port 465 3. server.login() authenticates with your App Password 4. server.send_message() delivers it

Keeping Secrets Safe with Environment Variables

Hardcoding passwords in your script is a bad habit. If you accidentally commit it to GitHub, bots will scrape it within minutes. Use environment variables instead:

import os
import smtplib
from email.mime.text import MIMEText

SENDER = os.environ["GMAIL_ADDRESS"]
PASSWORD = os.environ["GMAIL_APP_PASSWORD"]
RECEIVER = os.environ.get("REPORT_RECIPIENT", SENDER)

Now run your script with:

GMAIL_ADDRESS="you@gmail.com" \
GMAIL_APP_PASSWORD="your-app-password" \
python send_email.py

This way, your credentials never touch the codebase. If you've used cron (as covered in our Cron Jobs guide), you can set these in your crontab:

GMAIL_ADDRESS=you@gmail.com
GMAIL_APP_PASSWORD=xxxx
0 8 * * * /usr/bin/python3 /home/user/daily-report.py

Sending to Multiple Recipients

Want to send the same email to multiple people? Just set the To field as a comma-separated string, and pass a list to send_message:

recipients = ["alice@example.com", "bob@example.com"]

msg = MIMEText("Monthly report attached.")
msg["Subject"] = "June Report"
msg["From"] = SENDER
msg["To"] = ", ".join(recipients)  # "alice@example.com, bob@example.com"

with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
    server.login(SENDER, PASSWORD)
    server.sendmail(SENDER, recipients, msg.as_string())

Note: sendmail() (not send_message()) when you need to specify recipients separately from the header.

HTML Emails That Look Professional

Plain text is fine for alerts, but reports and newsletters deserve formatting:

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

msg = MIMEMultipart("alternative")
msg["Subject"] = "Weekly Price Report"
msg["From"] = SENDER
msg["To"] = RECEIVER

# Plain-text fallback (for email clients that block HTML)
text = """
Weekly Price Report — June 8, 2026
===================================
- Coffee maker: $39.99 (down $10 from last week)
- Bluetooth speaker: $24.99 (no change)
"""

# HTML version
html = """
<html>
  <body>
    <h2>Weekly Price Report — June 8, 2026</h2>
    <table border="1" cellpadding="8" cellspacing="0" style="border-collapse: collapse;">
      <tr style="background: #f0f0f0;">
        <th>Product</th><th>Current Price</th><th>Change</th>
      </tr>
      <tr>
        <td>Coffee Maker</td>
        <td style="color: green;">$39.99</td>
        <td>↓ $10.00</td>
      </tr>
      <tr>
        <td>Bluetooth Speaker</td>
        <td>$24.99</td>
        <td>—</td>
      </tr>
    </table>
    <p><em>Generated by Python. Questions? Reply to this email.</em></p>
  </body>
</html>
"""

# Attach both — email client picks the best one it can render
msg.attach(MIMEText(text, "plain"))
msg.attach(MIMEText(html, "html"))

with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
    server.login(SENDER, PASSWORD)
    server.send_message(msg)

Using MIMEMultipart("alternative") means the email client will display the HTML version but fall back to plain text if needed. This is the standard pattern for professional-looking automated emails.

Adding File Attachments

Reports, PDFs, images — Python can attach anything:

from email.mime.base import MIMEBase
from email import encoders

def attach_file(msg, filepath):
    """Attach a file to an email message."""
    with open(filepath, "rb") as f:
        part = MIMEBase("application", "octet-stream")
        part.set_payload(f.read())
        encoders.encode_base64(part)
        part.add_header(
            "Content-Disposition",
            f'attachment; filename="{os.path.basename(filepath)}"',
        )
        msg.attach(part)

# Usage
msg = MIMEMultipart()
msg["Subject"] = "Monthly Backup Report"
msg["From"] = SENDER
msg["To"] = RECEIVER
msg.attach(MIMEText("Backup completed successfully. Log attached."))

attach_file(msg, "/var/log/backup.log")
attach_file(msg, "/tmp/backup-summary.pdf")

Real Project: Price Drop Alert System

Let's combine everything into a real project. This script scrapes a product price (using our web scraping knowledge from the Python Web Scraping guide), compares it to yesterday's price, and emails you if it dropped:

"""price-alert.py — Check product price and email if it drops."""
import os
import json
import smtplib
import requests
from email.mime.text import MIMEText
from pathlib import Path

# ── Config ────────────────────────────────────────
PRODUCT_URL = "https://example.com/product/coffee-maker"
PRICE_FILE = Path.home() / ".price-history.json"
TARGET_PRICE = 45.00  # alert me if it drops below this
SENDER = os.environ["GMAIL_ADDRESS"]
PASSWORD = os.environ["GMAIL_APP_PASSWORD"]
RECEIVER = SENDER

# ── Get current price (simplified — real version would use BeautifulSoup) ──
def get_price():
    # In a real script, parse the HTML properly. See our web scraping guide!
    resp = requests.get(PRODUCT_URL)
    # ... extract price from HTML ...
    # For this example, returning a fake price:
    return 39.99

# ── Load yesterday's price ──
def load_history():
    if PRICE_FILE.exists():
        return json.loads(PRICE_FILE.read_text())
    return {}

# ── Save today's price ──
def save_history(history):
    PRICE_FILE.write_text(json.dumps(history, indent=2))

# ── Send alert ──
def send_alert(current, previous):
    drop = previous - current
    subject = f"💰 Price Drop Alert: ${previous} → ${current} (↓ ${drop:.2f})"
    body = f"""
    The product at {PRODUCT_URL}
    dropped from ${previous:.2f} to ${current:.2f}.
    You saved ${drop:.2f}!
    """
    msg = MIMEText(body)
    msg["Subject"] = subject
    msg["From"] = SENDER
    msg["To"] = RECEIVER

    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
        server.login(SENDER, PASSWORD)
        server.send_message(msg)
    print(f"Alert sent: {subject}")

# ── Main ──
history = load_history()
current = get_price()
previous = history.get("last_price", current)

print(f"Current: ${current:.2f}  |  Previous: ${previous:.2f}")

if current < previous:
    send_alert(current, previous)
elif current <= TARGET_PRICE:
    send_alert(current, previous)

history["last_price"] = current
history["last_check"] = str(requests.get)  # simplified
save_history(history)

Schedule it with cron:

0 8 * * * /usr/bin/python3 /home/user/price-alert.py >> /var/log/price-alert.log 2>&1

Now every morning at 8 AM, it checks the price and emails you if it dropped. Set it and forget it.

Gmail Sending Limits

Gmail has limits for automated sending (as of 2026):

Limit Free Account Google Workspace
Daily quota 500 emails/day 2,000 emails/day
Rate ~1 email/second ~1 email/second

For personal automation and alerts, 500/day is more than enough. If you need to send bulk newsletters, use a dedicated service like Mailgun or SendGrid — Gmail will lock your account if you hit the limits too often.

Alternate Email Providers

Gmail is not your only option. The same smtplib code works with any provider:

Provider SMTP Server Port Notes
Gmail smtp.gmail.com 465 (SSL) or 587 (TLS) Requires App Password
Outlook smtp.office365.com 587 (STARTTLS) Requires app password
Yahoo smtp.mail.yahoo.com 465 or 587 App password needed
QQ Mail smtp.qq.com 465 or 587 Popular in China, needs authorization code

For Outlook, the code changes slightly:

with smtplib.SMTP("smtp.office365.com", 587) as server:
    server.starttls()  # upgrade to encrypted connection
    server.login(SENDER, PASSWORD)
    server.send_message(msg)

What's Next?

You now have all the pieces for a complete automation pipeline:

  1. Collect data (web scraping, file monitoring, API calls)
  2. Process it (Python analysis, comparisons, formatting)
  3. Deliver results (email reports, alerts, notifications)
  4. Schedule it (cron jobs — set and forget)

Check out these related guides to build out your automation toolkit:

Start with a simple "backup complete" email, then build up to automated reports. Nothing feels more satisfying than waking up to a report your code generated while you slept.