Stop Refreshing the Page. Let Python Watch It.

Everyone has a page they refresh too often: a product waiting for a price drop, a store waiting for a restock, an event page waiting for tickets. Manual refreshing wastes time and usually misses the moment — the change happens while you sleep.

In this guide you'll build a watchdog script: a small Python program that checks a website every 15 minutes, detects when the page changes, and sends you an email the moment it does. It runs forever in the background, and you go back to living your life.

Two skills you can bring along: sending email from Python (our email automation guide sets up Gmail app passwords — you'll need one here) and downloading web pages (our web scraping beginner's guide explains how requests and HTML work).

The Idea in One Paragraph

A web page is just text. If we download the page today and download it again tomorrow, we can compare the two. If they're identical, nothing changed. If they differ, something did — a new price, a "In Stock" button, a new announcement.

Comparing full pages is slow, so we use a shortcut called a hash: a fingerprint of the text. Same text always produces the same fingerprint; change one character and the fingerprint is completely different. The script stores the last fingerprint, fetches a fresh one on a schedule, and compares. Mismatch = email.

Before You Start

You need two things:

  1. Python 3 installed (any version from the last few years works)
  2. A Gmail app password — a special password for scripts. The Python email automation guide walks through creating one in 3 minutes (Gmail → Security → 2-Step Verification → App passwords).

Only one library to install — requests (pip install requests). The other imports (smtplib, hashlib, time) come with Python.

pip install requests

The Complete Script

Create a file called watchdog.py and paste this in:

#!/usr/bin/env python3
"""
watchdog.py — email me when a web page changes.
Run it: python3 watchdog.py
Stop it: press Ctrl+C
"""
import hashlib
import smtplib
import time
from email.mime.text import MIMEText

import requests

# ---------- Settings — change these ----------
URL = "https://example.com/product-page"   # the page to watch
CHECK_EVERY = 900                           # seconds between checks (15 min)
HASH_FILE = "last_hash.txt"                 # remembers the last version
GMAIL_USER = "yourname@gmail.com"           # sender address
APP_PASSWORD = "xxxx xxxx xxxx xxxx"        # Gmail app password (see guide)
TO_EMAIL = "yourname@gmail.com"             # where alerts go
# ---------------------------------------------

def get_page_hash():
    """Download the page and return a fingerprint of its content."""
    response = requests.get(URL, timeout=30)
    response.raise_for_status()   # stop if the site is down or blocks us
    return hashlib.sha256(response.text.encode()).hexdigest()

def load_old_hash():
    """Return the last known fingerprint (None on the very first run)."""
    try:
        with open(HASH_FILE) as f:
            return f.read().strip()
    except FileNotFoundError:
        return None

def save_hash(new_hash):
    with open(HASH_FILE, "w") as f:
        f.write(new_hash)

def send_alert():
    """Email ourselves that the page changed."""
    subject = f"Website changed: {URL}"
    body = f"The page at {URL} just changed.\n\nGo check it: {URL}"
    message = MIMEText(body)
    message["Subject"] = subject
    message["From"] = GMAIL_USER
    message["To"] = TO_EMAIL
    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
        server.login(GMAIL_USER, APP_PASSWORD)
        server.send_message(message)
    print("Alert email sent!")

old_hash = load_old_hash()
print(f"Watching {URL} every {CHECK_EVERY} seconds. Press Ctrl+C to stop.")

while True:
    try:
        new_hash = get_page_hash()
        if old_hash is None:
            print("First check done. Now watching for changes...")
        elif new_hash != old_hash:
            print("Change detected!")
            send_alert()
        else:
            print("No change yet.")
        old_hash = new_hash
        save_hash(old_hash)
    except requests.exceptions.RequestException as e:
        print(f"Could not reach the site ({e}). Will retry.")
    time.sleep(CHECK_EVERY)

When you run python3 watchdog.py, you'll see:

Watching https://example.com/product-page every 900 seconds. Press Ctrl+C to stop.
First check done. Now watching for changes...
No change yet.
No change yet.
Change detected!
Alert email sent!

How to test it before trusting it: point URL at a page you can edit (a Google Doc published to the web, a GitHub file, or your own site). Run the script once, then edit the page and wait for the next check — you should get an email within 15 minutes. For a faster test, temporarily set CHECK_EVERY = 30.

How the Script Works, Line by Line

You can copy-paste without understanding it, but five minutes of reading makes debugging ten times easier:

  • get_page_hash() downloads the page with requests.get() and turns the text into a 64-character fingerprint with hashlib.sha256(). Same page → same fingerprint, guaranteed.
  • load_old_hash() / save_hash() read and write last_hash.txt. This tiny file is the script's memory — it's how the script "remembers" yesterday's page across restarts.
  • The while True loop is the heartbeat: fetch, compare, email, sleep, repeat — forever, until you press Ctrl+C.
  • try/except keeps the script alive when the site is down or your internet drops. Instead of crashing, it prints a note and retries after the next sleep.
  • send_alert() is the same Gmail code from our email automation guide — login with an app password, send a plain-text message, log out.

Fixing False Alarms: Pages That "Change" Constantly

Run the script on a news site or a store page and you may get an email every 15 minutes even though nothing meaningful changed. That's because pages carry timestamps, ads, and counters that change on every load — the hash sees all of it.

The fix: watch only the part you care about, not the whole page. This version extracts just the price element before hashing:

from bs4 import BeautifulSoup   # pip install beautifulsoup4

def get_page_hash():
    response = requests.get(URL, timeout=30)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")
    price = soup.select_one(".price").get_text(strip=True)   # just the price
    return hashlib.sha256(price.encode()).hexdigest()

select_one(".price") finds the first element with the CSS class price — right-click the price on your target page, choose "Inspect," and look for its class name. The web scraping guide explains selectors in detail.

Be a Polite Watcher

Checking a page every 15 minutes is 96 requests per day — small sites notice that. Follow these rules and you'll stay on the right side of everyone:

  • Keep CHECK_EVERY at 15+ minutes (10 minutes absolute minimum)
  • Check the site's robots.txt (add /robots.txt to the site URL) — if it disallows scraping, respect it
  • Watch public pages only — never pages that require a login
  • One script, one site — don't monitor 30 sites from the same machine

When Emails Don't Arrive: Quick Troubleshooting

Symptom Likely Cause Fix
535 login error App password typo or 2FA not set up Recreate the app password (see the email guide)
SSLError or connection refused Site blocks script requests Try another page, or add a browser-like User-Agent header to requests.get()
Alert lands in spam Gmail distrusts the sender (you) Mark one alert as "Not spam" — Gmail learns fast
"No change yet" forever, but the page clearly changed Page content loads via JavaScript, so the raw HTML never changes Use the BeautifulSoup approach anyway, or pick a simpler page
Email every 15 minutes Page has dynamic content the hash sees Use the select_one() fix above to watch only the part you care about

Run It 24/7

The script only watches while it's running. Three ways to keep it alive:

Setup Best For How
Your PC always on Casual use Just leave the terminal open
Linux server / Raspberry Pi Always-on watching Cron or systemd — see below
Windows Casual use Task Scheduler — see below

On Windows, press Win+R, type taskschd.msc, then: Create Basic Task → Trigger: When I log on → Action: Start a program → Program: python, Arguments: the full path to watchdog.py (like C:\Users\you\watchdog\watchdog.py). The script will watch quietly whenever you're logged in.

On a Linux machine (or your new CasaOS home server!), schedule it with cron:

crontab -e
# Check the page every 15 minutes, log output to a file:
*/15 * * * * cd /home/yourname/watchdog && python3 watchdog.py >> watchdog.log 2>&1

New to cron syntax? Our cron beginner's guide explains every field. (If the page hasn't changed, the script's "No change yet." lines will fill the log file — delete watchdog.log monthly.)

What to Watch: Five Real Uses

Use Page to Watch Change You're Waiting For
Price drops Product page The price element changes
Restocks Store product page "Out of stock" → "In stock"
Tickets Event page "Sold out" disappears
New articles Blog or news index A new headline appears
Announcements Company newsroom A new post is added

Next Steps

You now have a personal alert system that never sleeps. To keep building:

Set it up tonight, and tomorrow morning the internet will have emailed you.

All code in this article was tested and runs successfully on Python 3.8.10 (Ubuntu 20.04) with requests 2.32.4, beautifulsoup4 4.8.2 — verified August 2026.