Build a Price Monitoring Bot with Python: Never Miss a Deal
You've had your eye on that new laptop. Or camera. Or gadget. You keep checking the price, hoping it'll drop during a sale. But you can't check every day. And when it does go on sale, you miss it.
Let's build a Python bot that watches prices for you and sends an alert the moment they drop below your target.
What You'll Learn
- Web scraping for price extraction
- Storing and comparing price history
- Sending email and SMS alerts
- Scheduling automated price checks
- Handling multiple products and sites
Prerequisites
- Python 3.8 or higher
- requests library (
pip install requests) - BeautifulSoup (
pip install beautifulsoup4) - Basic understanding of HTML structure
The Problem
Manual price monitoring is tedious:
- Checking multiple sites daily takes time
- You forget to check and miss sales
- Price history disappears when you're not tracking
- Flash sales happen when you're not looking
The Solution
A Python bot that:
- Checks prices automatically on a schedule
- Stores price history to track trends
- Sends alerts when prices drop
- Works across multiple products and sites
Step 1: Basic Price Scraping
First, let's extract a price from a product page:
1import requests2from bs4 import BeautifulSoup34def get_page(url):5 """6 Fetch a web page with proper headers.78 Args:9 url: The URL to fetch1011 Returns:12 BeautifulSoup object or None if failed13 """14 headers = {15 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',16 'Accept-Language': 'en-US,en;q=0.9',17 }1819 try:20 response = requests.get(url, headers=headers, timeout=15)21 response.raise_for_status()22 return BeautifulSoup(response.content, 'html.parser')23 except requests.RequestException as e:24 print(f"Error fetching {url}: {e}")25 return None262728def extract_price(soup, price_selector):29 """30 Extract price from page using CSS selector.3132 Args:33 soup: BeautifulSoup object34 price_selector: CSS selector for price element3536 Returns:37 Float price or None if not found38 """39 import re4041 try:42 price_element = soup.select_one(price_selector)43 if not price_element:44 return None4546 price_text = price_element.get_text()4748 # Extract number from price string49 # Handles: $1,299.99, €99.99, £50.00, etc.50 match = re.search(r'[\d,]+\.?\d*', price_text.replace(',', ''))51 if match:52 return float(match.group().replace(',', ''))5354 except Exception as e:55 print(f"Error extracting price: {e}")5657 return None585960# Example usage61url = "https://example.com/product"62soup = get_page(url)63if soup:64 price = extract_price(soup, ".product-price")65 print(f"Current price: ${price}")
Step 2: Product Configuration
Define what products to monitor and their price targets:
1# products.py - Configuration for products to monitor23PRODUCTS = [4 {5 "name": "Sony WH-1000XM5 Headphones",6 "url": "https://www.amazon.com/dp/B09XS7JWHH",7 "price_selector": "span.a-price-whole",8 "target_price": 300.00,9 "check_enabled": True,10 },11 {12 "name": "MacBook Air M2",13 "url": "https://www.apple.com/shop/buy-mac/macbook-air",14 "price_selector": "span.as-price-currentprice",15 "target_price": 999.00,16 "check_enabled": True,17 },18 {19 "name": "PS5 Console",20 "url": "https://www.walmart.com/ip/PlayStation-5/...",21 "price_selector": "[data-testid='price']",22 "target_price": 400.00,23 "check_enabled": True,24 },25]
Step 3: Price History Storage
Store prices over time to track trends:
1import json2from datetime import datetime3from pathlib import Path456class PriceHistory:7 """Store and manage price history."""89 def __init__(self, history_file="price_history.json"):10 self.history_file = Path(history_file)11 self.data = self._load()1213 def _load(self):14 """Load existing history or create empty."""15 if self.history_file.exists():16 with open(self.history_file, 'r') as f:17 return json.load(f)18 return {}1920 def _save(self):21 """Save history to file."""22 with open(self.history_file, 'w') as f:23 json.dump(self.data, f, indent=2)2425 def add_price(self, product_name, price):26 """27 Add a price point for a product.2829 Args:30 product_name: Name of the product31 price: Current price32 """33 if product_name not in self.data:34 self.data[product_name] = {35 "prices": [],36 "lowest_price": None,37 "highest_price": None,38 }3940 entry = {41 "price": price,42 "timestamp": datetime.now().isoformat(),43 }4445 self.data[product_name]["prices"].append(entry)4647 # Update min/max48 prices = [p["price"] for p in self.data[product_name]["prices"]]49 self.data[product_name]["lowest_price"] = min(prices)50 self.data[product_name]["highest_price"] = max(prices)5152 self._save()5354 def get_lowest_price(self, product_name):55 """Get the lowest recorded price for a product."""56 if product_name in self.data:57 return self.data[product_name].get("lowest_price")58 return None5960 def get_price_trend(self, product_name, days=7):61 """62 Get price trend for last N days.6364 Returns: 'down', 'up', 'stable', or None65 """66 if product_name not in self.data:67 return None6869 prices = self.data[product_name]["prices"]70 if len(prices) < 2:71 return None7273 recent = prices[-1]["price"]74 older = prices[0]["price"]7576 if recent < older * 0.95: # 5% decrease77 return "down"78 elif recent > older * 1.05: # 5% increase79 return "up"80 return "stable"
Step 4: Alert System
Send notifications when prices drop:
1import smtplib2from email.mime.text import MIMEText3from email.mime.multipart import MIMEMultipart456def send_email_alert(product, current_price, target_price, lowest_ever):7 """8 Send email alert for price drop.910 Args:11 product: Product configuration dictionary12 current_price: Current price13 target_price: Target price threshold14 lowest_ever: Lowest recorded price15 """16 sender_email = "your.email@gmail.com"17 sender_password = "your-app-password" # Use environment variable!18 recipient = "your.email@gmail.com"1920 # Determine if this is a new low21 is_new_low = lowest_ever is None or current_price < lowest_ever2223 subject = f"🔥 Price Alert: {product['name']} - ${current_price:.2f}"2425 body = f"""26 <html>27 <body style="font-family: Arial, sans-serif;">28 <h2>Price Alert!</h2>2930 <h3>{product['name']}</h3>3132 <p style="font-size: 24px; color: #28a745;">33 Current Price: <strong>${current_price:.2f}</strong>34 </p>3536 <table style="border-collapse: collapse; margin: 20px 0;">37 <tr>38 <td style="padding: 8px; border: 1px solid #ddd;">Target Price:</td>39 <td style="padding: 8px; border: 1px solid #ddd;">${target_price:.2f}</td>40 </tr>41 <tr>42 <td style="padding: 8px; border: 1px solid #ddd;">Savings:</td>43 <td style="padding: 8px; border: 1px solid #ddd; color: #28a745;">44 ${target_price - current_price:.2f} ({((target_price - current_price) / target_price * 100):.1f}% below target)45 </td>46 </tr>47 {"<tr><td style='padding: 8px; border: 1px solid #ddd;'>🎉</td><td style='padding: 8px; border: 1px solid #ddd; color: #dc3545;'><strong>NEW LOWEST PRICE!</strong></td></tr>" if is_new_low else ""}48 </table>4950 <p><a href="{product['url']}" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Product</a></p>5152 <hr>53 <p style="color: #666; font-size: 12px;">54 Sent by Price Monitor Bot at {datetime.now().strftime('%Y-%m-%d %H:%M')}55 </p>56 </body>57 </html>58 """5960 msg = MIMEMultipart('alternative')61 msg['Subject'] = subject62 msg['From'] = sender_email63 msg['To'] = recipient64 msg.attach(MIMEText(body, 'html'))6566 try:67 with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:68 server.login(sender_email, sender_password)69 server.send_message(msg)70 print(f"✅ Alert sent for {product['name']}")71 return True72 except Exception as e:73 print(f"❌ Failed to send alert: {e}")74 return False
Step 5: The Main Monitor
Bring it all together:
1#!/usr/bin/env python32"""3Price Monitor Bot - Track prices and get alerts when they drop.4Author: Alex Rodriguez56Monitor product prices across multiple websites and receive7notifications when prices fall below your target.8"""910import json11import logging12import os13import re14import smtplib15import time16from datetime import datetime17from email.mime.multipart import MIMEMultipart18from email.mime.text import MIMEText19from pathlib import Path2021import requests22from bs4 import BeautifulSoup232425# ========================================26# CONFIGURATION27# ========================================2829# Products to monitor30PRODUCTS = [31 {32 "name": "Example Product",33 "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",34 "price_selector": "p.price_color",35 "target_price": 50.00,36 "check_enabled": True,37 },38 # Add more products here...39]4041# Alert settings42EMAIL_ALERTS = True43EMAIL_ADDRESS = os.environ.get("EMAIL_ADDRESS", "your.email@gmail.com")44EMAIL_PASSWORD = os.environ.get("EMAIL_PASSWORD", "your-app-password")45ALERT_RECIPIENT = os.environ.get("ALERT_RECIPIENT", "your.email@gmail.com")4647# File paths48HISTORY_FILE = "price_history.json"49LOG_FILE = "price_monitor.log"505152# ========================================53# LOGGING SETUP54# ========================================5556logging.basicConfig(57 level=logging.INFO,58 format='%(asctime)s | %(levelname)-8s | %(message)s',59 handlers=[60 logging.FileHandler(LOG_FILE),61 logging.StreamHandler()62 ]63)64logger = logging.getLogger(__name__)656667# ========================================68# PRICE HISTORY MANAGEMENT69# ========================================7071class PriceHistory:72 """Store and manage price history."""7374 def __init__(self, filepath=HISTORY_FILE):75 self.filepath = Path(filepath)76 self.data = self._load()7778 def _load(self):79 if self.filepath.exists():80 with open(self.filepath, 'r') as f:81 return json.load(f)82 return {}8384 def _save(self):85 with open(self.filepath, 'w') as f:86 json.dump(self.data, f, indent=2)8788 def add_price(self, product_name, price, url):89 if product_name not in self.data:90 self.data[product_name] = {91 "url": url,92 "prices": [],93 "lowest_price": None,94 "highest_price": None,95 }9697 entry = {98 "price": price,99 "timestamp": datetime.now().isoformat(),100 }101102 self.data[product_name]["prices"].append(entry)103104 # Keep only last 100 entries per product105 if len(self.data[product_name]["prices"]) > 100:106 self.data[product_name]["prices"] = self.data[product_name]["prices"][-100:]107108 prices = [p["price"] for p in self.data[product_name]["prices"]]109 self.data[product_name]["lowest_price"] = min(prices)110 self.data[product_name]["highest_price"] = max(prices)111112 self._save()113 return self.data[product_name]114115 def get_lowest(self, product_name):116 if product_name in self.data:117 return self.data[product_name].get("lowest_price")118 return None119120 def get_last_price(self, product_name):121 if product_name in self.data:122 prices = self.data[product_name].get("prices", [])123 if len(prices) >= 2:124 return prices[-2]["price"]125 return None126127128# ========================================129# WEB SCRAPING130# ========================================131132def fetch_page(url):133 """Fetch a web page with proper headers."""134 headers = {135 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',136 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',137 'Accept-Language': 'en-US,en;q=0.9',138 'Accept-Encoding': 'gzip, deflate, br',139 'Connection': 'keep-alive',140 }141142 try:143 response = requests.get(url, headers=headers, timeout=15)144 response.raise_for_status()145 return BeautifulSoup(response.content, 'html.parser')146 except requests.Timeout:147 logger.error(f"Timeout fetching {url}")148 except requests.HTTPError as e:149 logger.error(f"HTTP error {e.response.status_code} for {url}")150 except requests.RequestException as e:151 logger.error(f"Error fetching {url}: {e}")152153 return None154155156def extract_price(soup, selector):157 """Extract price from page using CSS selector."""158 try:159 element = soup.select_one(selector)160 if not element:161 return None162163 text = element.get_text()164165 # Extract numeric price166 # Handles various formats: $1,299.99, £50.00, €99,99167 cleaned = re.sub(r'[^\d.,]', '', text)168169 # Handle European format (comma as decimal)170 if ',' in cleaned and '.' in cleaned:171 if cleaned.index('.') < cleaned.index(','):172 cleaned = cleaned.replace('.', '').replace(',', '.')173 else:174 cleaned = cleaned.replace(',', '')175 elif ',' in cleaned and '.' not in cleaned:176 # Could be European decimal or thousands separator177 if len(cleaned.split(',')[-1]) == 2:178 cleaned = cleaned.replace(',', '.')179 else:180 cleaned = cleaned.replace(',', '')181182 return float(cleaned) if cleaned else None183184 except Exception as e:185 logger.error(f"Error extracting price: {e}")186187 return None188189190def get_title(soup):191 """Try to extract product title from page."""192 selectors = ['h1', '.product-title', '.product-name', '#productTitle']193194 for selector in selectors:195 element = soup.select_one(selector)196 if element:197 return element.get_text().strip()[:100]198199 return None200201202# ========================================203# ALERTS204# ========================================205206def send_email_alert(product, current_price, target_price, lowest_ever, is_new_low):207 """Send email alert for price drop."""208 if not EMAIL_ALERTS:209 return210211 savings = target_price - current_price212 savings_pct = (savings / target_price) * 100213214 subject = f"🔥 Price Alert: {product['name']} - ${current_price:.2f}"215216 body = f"""217 <html>218 <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">219 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center;">220 <h1 style="margin: 0;">💰 Price Drop Alert!</h1>221 </div>222223 <div style="padding: 20px;">224 <h2>{product['name']}</h2>225226 <div style="background-color: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px; margin: 20px 0;">227 <p style="margin: 0; font-size: 28px; color: #155724;">228 ${current_price:.2f}229 </p>230 <p style="margin: 5px 0 0 0; color: #155724;">231 Save ${savings:.2f} ({savings_pct:.1f}% below target!)232 </p>233 </div>234235 {"<p style='background-color: #fff3cd; padding: 10px; border-radius: 5px;'>🎉 <strong>NEW ALL-TIME LOW!</strong></p>" if is_new_low else ""}236237 <table style="width: 100%; border-collapse: collapse; margin: 20px 0;">238 <tr>239 <td style="padding: 10px; border-bottom: 1px solid #ddd;">Your Target Price</td>240 <td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">${target_price:.2f}</td>241 </tr>242 <tr>243 <td style="padding: 10px; border-bottom: 1px solid #ddd;">Lowest Recorded</td>244 <td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">${lowest_ever:.2f if lowest_ever else 'N/A'}</td>245 </tr>246 </table>247248 <p style="text-align: center;">249 <a href="{product['url']}" style="display: inline-block; background-color: #28a745; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; font-weight: bold;">250 🛒 Buy Now251 </a>252 </p>253 </div>254255 <div style="background-color: #f8f9fa; padding: 15px; text-align: center; font-size: 12px; color: #666;">256 Price Monitor Bot • {datetime.now().strftime('%Y-%m-%d %H:%M')}257 </div>258 </body>259 </html>260 """261262 msg = MIMEMultipart('alternative')263 msg['Subject'] = subject264 msg['From'] = EMAIL_ADDRESS265 msg['To'] = ALERT_RECIPIENT266 msg.attach(MIMEText(body, 'html'))267268 try:269 with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:270 server.login(EMAIL_ADDRESS, EMAIL_PASSWORD)271 server.send_message(msg)272 logger.info(f"📧 Alert sent for {product['name']}")273 return True274 except Exception as e:275 logger.error(f"Failed to send email: {e}")276 return False277278279# ========================================280# MAIN MONITOR281# ========================================282283def check_product(product, history):284 """285 Check a single product's price.286287 Returns: Dictionary with check results288 """289 logger.info(f"Checking: {product['name']}")290291 result = {292 "name": product['name'],293 "url": product['url'],294 "target_price": product['target_price'],295 "current_price": None,296 "status": "unknown",297 "alert_sent": False,298 }299300 # Fetch and parse page301 soup = fetch_page(product['url'])302 if not soup:303 result["status"] = "fetch_failed"304 return result305306 # Extract price307 price = extract_price(soup, product['price_selector'])308 if price is None:309 result["status"] = "price_not_found"310 logger.warning(f" Could not extract price for {product['name']}")311 return result312313 result["current_price"] = price314 logger.info(f" Current price: ${price:.2f} (target: ${product['target_price']:.2f})")315316 # Get historical data317 lowest_ever = history.get_lowest(product['name'])318 last_price = history.get_last_price(product['name'])319320 # Record this price321 history.add_price(product['name'], price, product['url'])322323 # Check if this is a new low324 is_new_low = lowest_ever is None or price < lowest_ever325326 # Determine if we should alert327 if price <= product['target_price']:328 result["status"] = "below_target"329 logger.info(f" 🎯 BELOW TARGET! (${price:.2f} <= ${product['target_price']:.2f})")330331 # Send alert332 alert_sent = send_email_alert(333 product, price, product['target_price'], lowest_ever, is_new_low334 )335 result["alert_sent"] = alert_sent336337 elif is_new_low:338 result["status"] = "new_low"339 logger.info(f" 📉 New lowest price recorded: ${price:.2f}")340341 elif last_price and price < last_price:342 result["status"] = "price_dropped"343 logger.info(f" ↓ Price dropped from ${last_price:.2f}")344345 else:346 result["status"] = "above_target"347348 return result349350351def run_monitor():352 """Run the price monitor for all products."""353 logger.info("=" * 60)354 logger.info("PRICE MONITOR STARTING")355 logger.info(f"Time: {datetime.now()}")356 logger.info(f"Products to check: {len([p for p in PRODUCTS if p['check_enabled']])}")357 logger.info("=" * 60)358359 history = PriceHistory()360 results = []361362 for product in PRODUCTS:363 if not product.get('check_enabled', True):364 continue365366 result = check_product(product, history)367 results.append(result)368369 # Be polite - wait between requests370 time.sleep(2)371372 # Summary373 logger.info("\n" + "=" * 60)374 logger.info("SUMMARY")375 logger.info("=" * 60)376377 alerts_sent = sum(1 for r in results if r['alert_sent'])378 below_target = sum(1 for r in results if r['status'] == 'below_target')379 failed = sum(1 for r in results if r['status'] in ['fetch_failed', 'price_not_found'])380381 logger.info(f"Products checked: {len(results)}")382 logger.info(f"Below target price: {below_target}")383 logger.info(f"Alerts sent: {alerts_sent}")384 logger.info(f"Failed checks: {failed}")385386 return results387388389def main():390 """Main entry point."""391 try:392 run_monitor()393 except KeyboardInterrupt:394 logger.info("\nMonitor stopped by user")395 except Exception as e:396 logger.exception(f"Monitor failed: {e}")397 raise398399400if __name__ == "__main__":401 main()
How to Run This Script
-
Install dependencies:
bash1pip install requests beautifulsoup4 -
Configure products in the
PRODUCTSlist:- Get the product URL
- Find the price selector (right-click price → Inspect)
- Set your target price
-
Set up email alerts:
bash1export EMAIL_ADDRESS="your.email@gmail.com"2export EMAIL_PASSWORD="your-app-password"3export ALERT_RECIPIENT="your.email@gmail.com" -
Run manually:
bash1python price_monitor.py -
Schedule automatic checks (see our scheduling guide):
bash1# Run every 6 hours20 */6 * * * /usr/bin/python3 /path/to/price_monitor.py
Finding Price Selectors
- Open the product page in your browser
- Right-click on the price → Inspect
- Look for the HTML element containing the price
- Find a unique identifier:
- Class:
.product-price,.a-price-whole - ID:
#priceblock_ourprice - Data attribute:
[data-price]
- Class:
- Test in browser console:
document.querySelector(".your-selector")
Customization Options
Add Multiple Alert Thresholds
1PRODUCTS = [2 {3 "name": "Product",4 "url": "...",5 "price_selector": "...",6 "target_price": 100,7 "alert_thresholds": [8 {"price": 100, "message": "Good deal!"},9 {"price": 80, "message": "Great deal!"},10 {"price": 60, "message": "AMAZING DEAL!"},11 ],12 },13]
Track Price History Graph
1import matplotlib.pyplot as plt23def plot_price_history(product_name, history):4 """Generate price history chart."""5 data = history.data.get(product_name, {})6 prices = data.get("prices", [])78 if not prices:9 return1011 dates = [p["timestamp"][:10] for p in prices]12 values = [p["price"] for p in prices]1314 plt.figure(figsize=(10, 5))15 plt.plot(dates, values, marker='o')16 plt.title(f"Price History: {product_name}")17 plt.xlabel("Date")18 plt.ylabel("Price ($)")19 plt.xticks(rotation=45)20 plt.tight_layout()21 plt.savefig(f"{product_name}_history.png")
Common Issues & Solutions
| Issue | Solution |
|---|---|
| Price not found | Check selector; site may use JavaScript (try Selenium) |
| Getting blocked | Add delays; rotate User-Agents; use proxies |
| Wrong price extracted | Selector may match multiple elements; be more specific |
| Email alerts fail | Check Gmail app password; verify credentials |
| Prices in wrong format | Adjust the price parsing regex for your locale |
Taking It Further
Add Browser Automation for JavaScript Sites
1# pip install selenium webdriver-manager2from selenium import webdriver3from selenium.webdriver.chrome.service import Service4from webdriver_manager.chrome import ChromeDriverManager56def fetch_js_page(url):7 """Fetch page that requires JavaScript."""8 options = webdriver.ChromeOptions()9 options.add_argument('--headless')1011 driver = webdriver.Chrome(12 service=Service(ChromeDriverManager().install()),13 options=options14 )1516 try:17 driver.get(url)18 time.sleep(3) # Wait for JS to load19 return BeautifulSoup(driver.page_source, 'html.parser')20 finally:21 driver.quit()
SMS Alerts via Twilio
1# pip install twilio2from twilio.rest import Client34def send_sms_alert(message):5 client = Client("ACCOUNT_SID", "AUTH_TOKEN")6 client.messages.create(7 body=message,8 from_="+1234567890",9 to="+0987654321"10 )
Conclusion
You now have a complete price monitoring system. It checks prices automatically, stores history for trend analysis, and alerts you the moment prices drop below your targets.
Never miss a deal again. Set your target prices, schedule the script, and let Python do the watching. When that price drops, you'll be the first to know.
Your personal shopping assistant, automated.
Sponsored Content
Interested in advertising? Reach automation professionals through our platform.
