Python classroom demo

Website monitor that spots a restock and sends a notification.

This demo page is built to help students connect a real-world automation idea to a readable Python script: check a product page, detect when stock returns, and send one clear alert.

Monitor snapshotWatching every 90s

Tracked item

Quiet Mechanical Keyboard

$129.00

Back in stock detected

08:00 - product still sold out

09:30 - product page changed

09:30 - notification sent

11:00 - product still in stock, no duplicate alert

Librariesrequests, BeautifulSoup, smtplib
Core ideapoll -> compare -> notify
Best for classintro automation, web parsing, persistence

The monitoring loop in plain English

1. Request the page

Use requests to download the product page on a timer.

2. Inspect the HTML

Use BeautifulSoup to find the title, price, and stock label.

3. Detect the state change

Compare the current result with the last saved result.

4. Alert once

Send an email only when the item flips from sold out to available.

A complete demo script students can walk through line by line

Update the selectors for the target site and add email credentials through environment variables when you want real alerts.

import json
import os
import smtplib
import ssl
import time
from dataclasses import dataclass
from email.message import EmailMessage

import requests
from bs4 import BeautifulSoup

PRODUCT_URL = "https://example.com/products/quiet-keyboard"
CHECK_EVERY_SECONDS = 90
STATE_FILE = "monitor_state.json"
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/126.0 Safari/537.36"
    )
}


@dataclass
class ProductStatus:
    title: str
    in_stock: bool
    price: str


def fetch_status() -> ProductStatus:
    response = requests.get(PRODUCT_URL, headers=HEADERS, timeout=15)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "html.parser")
    title = soup.select_one("h1").get_text(strip=True)
    price = soup.select_one("[data-price]").get_text(" ", strip=True)
    stock_text = soup.select_one(".stock-status").get_text(" ", strip=True).lower()

    in_stock = "out of stock" not in stock_text and "sold out" not in stock_text
    return ProductStatus(title=title, in_stock=in_stock, price=price)


def load_previous_state() -> bool:
    if not os.path.exists(STATE_FILE):
        return False
    with open(STATE_FILE, "r", encoding="utf-8") as file:
        return json.load(file).get("was_in_stock", False)


def save_state(in_stock: bool) -> None:
    with open(STATE_FILE, "w", encoding="utf-8") as file:
        json.dump({"was_in_stock": in_stock}, file)


def send_notification(status: ProductStatus) -> None:
    sender = os.environ.get("ALERT_EMAIL")
    password = os.environ.get("ALERT_APP_PASSWORD")
    recipient = os.environ.get("ALERT_RECIPIENT", sender)

    subject = f"Back in stock: {status.title}"
    body = (
        f"{status.title} is available again.\n"
        f"Price: {status.price}\n"
        f"Link: {PRODUCT_URL}"
    )

    if not sender or not password or not recipient:
        print("EMAIL SETTINGS MISSING")
        print(body)
        return

    message = EmailMessage()
    message["Subject"] = subject
    message["From"] = sender
    message["To"] = recipient
    message.set_content(body)

    context = ssl.create_default_context()
    with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
        server.login(sender, password)
        server.send_message(message)


def monitor() -> None:
    was_in_stock = load_previous_state()

    while True:
        try:
            status = fetch_status()
            print(f"Checked {status.title}: in_stock={status.in_stock}")

            if status.in_stock and not was_in_stock:
                send_notification(status)

            save_state(status.in_stock)
            was_in_stock = status.in_stock
        except Exception as error:
            print(f"Monitor error: {error}")

        time.sleep(CHECK_EVERY_SECONDS)


if __name__ == "__main__":
    monitor()