Architecture
- Fetch product page (or API)
- Parse price
- Save price history (CSV/SQLite)
- Compare with threshold
- Notify (email/telegram)
Example: parse a price from HTML
from bs4 import BeautifulSoup
html = "<span class='price'>$199.99</span>"
soup = BeautifulSoup(html, "html.parser")
price_text = soup.select_one(".price").get_text(strip=True)
print(price_text)Persist history (CSV)
import csv
from datetime import datetime
def append_price(csv_path: str, price: str):
with open(csv_path, "a", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow([datetime.now().isoformat(), price])Notes
Real sites often block scraping.
Prefer APIs, and respect site policies.
Visualize it
A tracker bot loops through fetch, parse, and store steps, retrying politely when a request fails.
flowchart TD
A["Fetch the page"] --> B{"Request successful?"}
B -- "No" --> E["Wait/retry (respect rate limits)"]
E --> A
B -- "Yes" --> C["Parse the HTML"]
C --> D["Extract the price"]
D --> F["Store it"]
How the tracker fits together
A price tracker is a small loop that runs on a schedule. Each run does the same five things, and each is a separate function, so you can test the parts that do not need the network.
The parts that are pure Python are the most valuable to get right, and the price text is the trickiest. A web page shows $1,199.99 or 1.199,99 โฌ, and you need a number. Parse it in one function, and handle failure:
import csv
from datetime import datetime
from pathlib import Path
def parse_price(text: str) -> float:
cleaned = text.replace("$", "").replace(",", "").strip()
return float(cleaned) # raises ValueError if the layout changed
def last_price(csv_path: Path) -> float | None:
if not csv_path.exists():
return None
with csv_path.open(newline="", encoding="utf-8") as f:
rows = list(csv.reader(f))
return float(rows[-1][1]) if rows else None
def check(csv_path: Path, price_text: str, threshold: float) -> None:
price = parse_price(price_text)
previous = last_price(csv_path)
with csv_path.open("a", newline="", encoding="utf-8") as f:
csv.writer(f).writerow([datetime.now().isoformat(), price])
if price <= threshold and (previous is None or previous > threshold):
print(f"ALERT: price fell to {price} (threshold {threshold})")
check(Path("history.csv"), "$199.99", threshold=200)The alert fires only when the price crosses the threshold, not on every run while it stays below. Without that check you would get an email every hour for as long as the sale lasts.
Why store history
Storing every price, not just the latest, lets you see trends, spot a fake "discount", and debug. CSV is fine for one product. For many products, use SQLite (sqlite3 is in the standard library) with a table of product, time, and price.
The fetch step, done politely
Fetch with requests and a timeout, send a User-Agent that names your bot, and run at most a few times a day. Check robots.txt and the terms first. Many shops forbid scraping. If the store has an official API or a price feed, use that. Scrape only what the site allows, and stop if you get 403 or 429 responses.
Common mistakes
- Trusting a selector forever. Sites change their layout. When
select_onereturnsNone, log it and alert yourself instead of crashing silently. - Parsing without handling locale.
1.299,00and1,299.00mean the same thing but parse differently. - Alerting on every run. Track the previous state.
- Running every minute. That is rude to the site and gets you blocked. Hourly or daily is plenty.
- Storing the price as text. Store numbers so you can compare them.
Adapting it
Wire the alert into the email, Telegram, or Slack lessons, and run the script with cron, Task Scheduler, or the schedule library.
๐งช Try It Yourself
Exercise 1 โ Parse a Price and Compare
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.Check your understanding
Why does the tracker compare the new price with the previous one before alerting?
Alerting on the crossing, rather than on the state, gives one message when the price drops instead of one per run.
Show answer
A โ To avoid sending the same alert on every run while the price stays below the threshold โ Alerting on the crossing, rather than on the state, gives one message when the price drops instead of one per run.
select_one(".price") returns None after the site redesigns. What should the bot do?
A missing element is a signal that the page changed. Storing 0 would corrupt the history and could trigger false alerts.
Show answer
B โ Log the failure and notify you, rather than storing a wrong value โ A missing element is a signal that the page changed. Storing 0 would corrupt the history and could trigger false alerts.
How often should a price tracker fetch a single product page?
Frequent requests burden the site and get you blocked. Prices rarely change more than a few times a day.
Show answer
B โ Rarely, such as hourly or daily, and only where the site allows scraping โ Frequent requests burden the site and get you blocked. Prices rarely change more than a few times a day.