Have you ever refreshed a page over and over waiting for a price to drop, a product to come back in stock, or a news article to update? A website update notifier automates all of that for you. The script checks a URL at set intervals, compares the current page content against what it saw last time, and fires a desktop alert the moment anything changes.
This tutorial walks through building that script from scratch using four tools: the requests library to fetch pages, Python's built-in hashlib to fingerprint content, time.sleep() to pace the checks, and plyer to pop up a desktop notification. No prior experience with web scraping or automation is needed.
What the Script Actually Does
Before writing any code it helps to understand the flow at a high level. The notifier does three things on a loop:
- It downloads the HTML of a web page.
- It converts that HTML into a short fixed-length fingerprint called a hash.
- It compares the new hash to the hash from the last check. If they differ, the page changed and it sends you an alert.
The loop runs indefinitely, pausing for a set number of seconds between each check. You stop it by pressing Ctrl+C in the terminal.
Installing the Required Libraries
Python ships with hashlib and time already available, so you only need to install two external packages. Open your terminal and run:
pip install requests plyer
requests is the most widely used HTTP library in Python. It handles the mechanics of opening a connection to a server, sending a GET request, and returning the response so you can read the page content. plyer is a lightweight library that wraps each operating system's notification system — it works on Windows, macOS, and Linux without you needing to know the OS-specific API.
If you are using a virtual environment (recommended for any Python project), activate it first and then run the pip command above. This keeps the project's dependencies isolated from other Python projects on your machine.
Understanding the Core Concepts
Two concepts do the heavy lifting in this project: HTTP GET requests and hashing. It is worth understanding them before reading the code.
HTTP GET Requests
Every time your browser loads a web page it sends an HTTP GET request to the server at that URL. The server responds with the page HTML as plain text. requests.get(url) does exactly what your browser does, but instead of rendering the HTML it hands the raw text back to your Python code via response.text.
Hashing for Change Detection
A hash function takes an input of any length and returns a fixed-length string called a digest. If the input changes even by a single character, the digest changes completely. That makes hashes ideal for detecting whether content has been modified. Instead of storing and comparing two entire HTML documents, the script stores and compares two short strings like a3f5.... Python's hashlib module provides several hash algorithms; MD5 produces a 32-character hex string and is fast for non-security use cases like this one.
- Digest length
- 32 hex characters (128 bits)
- Good for
- Fast change detection where collision resistance is not required. Fine for this project.
- Digest length
- 64 hex characters (256 bits)
- Good for
- When you want stronger collision resistance. Slightly slower than MD5 but still very fast in practice.
- Digest length
- 40 hex characters (160 bits)
- Good for
- A middle ground between MD5 and SHA-256. Considered weak for cryptographic purposes but acceptable for content fingerprinting.
Writing the Hash Function
Create a new file called notifier.py. Start with the imports and one helper function that does two jobs — it fetches the page and returns its hash in a single call.
import requests
import hashlib
import time
from plyer import notification
# ── Configuration ────────────────────────────────────────────
URL = "https://example.com" # replace with the page you want to watch
INTERVAL = 60 # seconds between checks
def get_page_hash(url):
"""Fetch a URL and return the MD5 hash of its HTML content."""
response = requests.get(url, timeout=10)
response.raise_for_status() # raises an error on 4xx/5xx status
content = response.text.encode("utf-8")
return hashlib.md5(content).hexdigest()
Walk through what each line does:
requests.get(url, timeout=10)— sends a GET request. Thetimeout=10argument tellsrequeststo give up after 10 seconds if the server does not respond, rather than hanging forever.response.raise_for_status()— if the server returned a 404 or 500 status code, this turns that into a Python exception immediately rather than silently hashing an error page.response.text.encode("utf-8")—hashlibrequires bytes, not a string, so.encode()converts the HTML text to a bytes object.hashlib.md5(content).hexdigest()— creates an MD5 hash object, feeds it the content, and returns the result as a 32-character lowercase hex string.
To switch to SHA-256, change hashlib.md5(content) to hashlib.sha256(content). The rest of the code stays exactly the same. SHA-256 digests are 64 characters instead of 32.
Build the line of code that hashes a bytes variable called content using MD5 and returns the hex digest:
hashlib.md5(content).hexdigest(). You call hashlib.md5() with the bytes variable as the argument, which creates a hash object, then chain .hexdigest() to get the 32-character hex string. SHA-256 is a valid algorithm but the question specifically asks for MD5. Both method calls require their own pair of parentheses.
Building the Polling Loop
With the hash function in place, add the main loop below it in the same file.
def notify(url):
"""Pop up a desktop alert."""
notification.notify(
title = "Page Changed",
message = f"Content updated at:\n{url}",
timeout = 10 # notification disappears after 10 seconds
)
def run_notifier(url, interval):
print(f"Watching: {url}")
print(f"Checking every {interval} seconds. Press Ctrl+C to stop.\n")
last_hash = get_page_hash(url)
print(f"Baseline hash recorded: {last_hash}")
try:
while True:
time.sleep(interval)
current_hash = get_page_hash(url)
if current_hash != last_hash:
print(f"Change detected! New hash: {current_hash}")
notify(url)
last_hash = current_hash # update baseline
else:
print(f"No change. ({current_hash})")
except KeyboardInterrupt:
print("\nNotifier stopped.")
if __name__ == "__main__":
run_notifier(URL, INTERVAL)
The key design decisions here are worth noting. The baseline hash is recorded before the loop starts, not inside it. That way the very first iteration always has something to compare against. The last_hash variable is updated when a change is found so that the script tracks the most recent version of the page, not the original. Finally, wrapping the loop in try/except KeyboardInterrupt means pressing Ctrl+C prints a clean message instead of dumping a traceback to the terminal.
Spot the Bug
The function below tries to fetch a URL and return its MD5 hash. One line contains a mistake that would cause the hash to always be the same regardless of the page content. Find it.
url.encode("utf-8") to response.text.encode("utf-8"). Encoding the URL string instead of the response body means you are always hashing the same URL — which never changes — so the hash is always identical and changes to the page will never be detected.
Adding Error Handling
A production-ready notifier should not crash if the network is unavailable or the server returns an error. Update get_page_hash to catch exceptions and return None when a fetch fails, then update run_notifier to skip the comparison when that happens.
def get_page_hash(url):
"""Fetch a URL and return MD5 hash, or None on error."""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
content = response.text.encode("utf-8")
return hashlib.md5(content).hexdigest()
except requests.exceptions.RequestException as e:
print(f"Fetch error: {e}")
return None
Inside the loop, guard the comparison with a check for None:
current_hash = get_page_hash(url)
if current_hash is None:
print("Skipping comparison due to fetch error.")
continue # go back to the top of the while loop
if current_hash != last_hash:
print(f"Change detected!")
notify(url)
last_hash = current_hash
else:
print(f"No change.")
Always check a website's robots.txt and terms of service before running automated requests against it. A 60-second interval is considerate. Polling every few seconds can harm server performance and may get your IP address blocked.
How to Build a Website Update Notifier in Python
The following steps bring the entire script together from a blank file to a running notifier.
-
Install the required libraries
Run
pip install requests plyerin your terminal. Confirm the installation by runningpython -c "import requests, plyer; print('OK')"— you should seeOKprinted to the terminal. -
Create the file and write the imports
Create
notifier.pyand add the four import lines at the top:import requests,import hashlib,import time, andfrom plyer import notification. Then define yourURLandINTERVALconstants. -
Write the get_page_hash function
Add the
get_page_hash(url)function with atry/exceptblock around the fetch. Encoderesponse.textto UTF-8 bytes and returnhashlib.md5(content).hexdigest(). ReturnNonein the except branch. -
Write the polling loop
Add the
notify(url)function and thenrun_notifier(url, interval). Record the baseline hash before the loop, enterwhile True, calltime.sleep(interval)first, then fetch and compare. Wrap everything intry/except KeyboardInterrupt. -
Run the script and test it
Save the file and run
python notifier.py. To test a change detection, serve a local HTML file with Python's built-in server (python -m http.server 8000), pointURLathttp://localhost:8000, then edit the HTML file while the notifier is running. A desktop notification should appear within the next polling interval.
The Complete Script
Here is the full notifier.py with all sections assembled and error handling included.
import requests
import hashlib
import time
from plyer import notification
# ── Configuration ────────────────────────────────────────────
URL = "https://example.com" # replace with the page to watch
INTERVAL = 60 # seconds between checks
def get_page_hash(url):
"""Fetch a URL and return the MD5 hash of its HTML, or None on error."""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
content = response.text.encode("utf-8")
return hashlib.md5(content).hexdigest()
except requests.exceptions.RequestException as e:
print(f"Fetch error: {e}")
return None
def notify(url):
"""Send a desktop notification."""
notification.notify(
title = "Page Changed",
message = f"Content updated at:\n{url}",
timeout = 10
)
def run_notifier(url, interval):
print(f"Watching: {url}")
print(f"Checking every {interval}s. Press Ctrl+C to stop.\n")
last_hash = get_page_hash(url)
if last_hash is None:
print("Could not reach the URL. Check your connection and try again.")
return
print(f"Baseline: {last_hash}\n")
try:
while True:
time.sleep(interval)
current_hash = get_page_hash(url)
if current_hash is None:
print("Skipping — fetch failed.")
continue
if current_hash != last_hash:
print(f"Change detected! {current_hash}")
notify(url)
last_hash = current_hash
else:
print(f"No change. {current_hash}")
except KeyboardInterrupt:
print("\nNotifier stopped.")
if __name__ == "__main__":
run_notifier(URL, INTERVAL)
"Automate the boring stuff so your attention goes to what matters." — common Python community wisdom
What to Learn Next
Once this notifier runs reliably, several natural extensions build directly on the same skills:
- Use BeautifulSoup to parse the HTML and hash only a specific element — a price tag, a stock status badge, or a headline — rather than the entire page.
- Replace the
plyeralert with an email notification using Python'ssmtplibmodule so you receive alerts even when your computer is not running the script. - Monitor multiple URLs by storing them in a dictionary keyed by URL with their last known hash as the value, then iterating inside the loop.
- Schedule the script to run automatically at startup using a system tool like cron on Linux/macOS or Task Scheduler on Windows.
Summary
- Use
requests.get()with a timeout to fetch a page, and always callraise_for_status()to catch HTTP errors early. - Hash the encoded response text with
hashlib.md5().hexdigest()to get a short fingerprint that changes when the page content changes. - Record a baseline hash before the loop, then compare inside
while Trueusingtime.sleep()to control the polling rate. - Use
plyer.notification.notify()for cross-platform desktop alerts, and wrap the loop intry/except KeyboardInterruptfor a clean exit.
The complete script is under 50 lines. It uses four well-documented tools that you will encounter in many other Python automation projects, making this a strong foundation for more advanced work.
Frequently Asked Questions
You need the requests library to fetch web pages, hashlib (built into Python's standard library) to fingerprint page content, time (also built-in) to control the polling interval, and plyer to send cross-platform desktop notifications. Install requests and plyer with pip install requests plyer.
The script fetches the HTML content of a URL, runs it through MD5 or SHA-256 to produce a short fixed-length hash, then compares that hash to the one stored from the previous check. If the hashes differ, the page has changed and the script fires a desktop alert.
A polling loop is a while True loop that repeatedly executes a task, then pauses for a set number of seconds using time.sleep() before running again. In this project the loop fetches a URL and checks for changes on every iteration.
Hashing converts the entire HTML string into a short fixed-size fingerprint such as a 32-character hex string. Comparing two short strings is faster and uses less memory than comparing two full HTML documents that could be megabytes long.
Press Ctrl+C in the terminal where the script is running. Python raises a KeyboardInterrupt exception, which the try/except block catches to print a clean shutdown message instead of an error traceback.
Yes. Store your URLs in a list or dictionary, iterate over them inside the polling loop, and maintain a separate last_hash value for each URL. Each iteration checks every URL before sleeping.
The requests.get() call raises a requests.exceptions.RequestException. The try/except block in get_page_hash catches it, prints a warning, and returns None. The loop then skips the comparison for that iteration and tries again after the next sleep.
Parse the page HTML with the BeautifulSoup library, extract the specific element you want to watch using its CSS selector or tag name, then hash only that element's text instead of the entire page. This avoids false positives caused by unrelated parts of the page changing.
A check interval of 60 to 300 seconds (1 to 5 minutes) is reasonable for personal use. Checking too frequently can overload the target server, get your IP blocked, or violate a site's terms of service. Always check the site's robots.txt and terms before running automated requests.
Both can fetch URLs, but the requests library provides a simpler API, automatic handling of redirects, easy access to response content and status codes, and cleaner error handling. urllib.request is part of the standard library and requires no installation, but its syntax is more verbose for common tasks.