I’ve always been fascinated by how a simple ping or pop-up can transform the way I interact with my applications.
That’s because real-time alerts drive faster responses and better visibility into automation workflows—they let me know the moment a long-running script finishes, an error pops up, or an important event happens in one of my services.
In this article, I’ll walk you through everything you need to get started with ntfy.sh and Python:
- What notifications really are, and why they’ve become such an integral part of modern apps.
- An introduction to ntfy.sh, the lightweight, open-source notification service I’ve come to rely on.
- Key use cases where I’ve used ntfy.sh—from DevOps monitoring to home automation.
- A hands-on sample application, complete with Python code you can run today to send notifications to your phone and desktop browser.
By the end, you’ll have a working notification system powered by Python and ntfy.sh, ready to alert you wherever you go.
You can get the source code for this script at: https://github.com/nunombispo/Folder-Monitor-with-ntfy.sh
If you prefer a video version, check out the YouTube video:
SPONSORED By Python's Magic Methods - Beyond init and str
This book offers an in-depth exploration of Python's magic methods, examining the mechanics and applications that make these features essential to Python's design.
What Are Notifications?
I remember the first time I received a notification that wasn’t just an email or SMS—it popped up on my screen without me asking, and it felt like magic.
At its core, a push notification is exactly that: an alert “pushed” from a server directly to my device, even when my app isn’t actively requesting updates.
This means I don’t have to poll a service or refresh a page; instead, the server reaches out and says, “Hey, something happened!”
Over the years I’ve come to appreciate that notifications actually come in a few flavors.
Local notifications are scheduled by the app on the device itself—think reminders you set in a to-do app.
Remote notifications, on the other hand, are triggered by an external server (the classic push notification scenario).
And then you can split those further by platform: mobile notifications on iOS or Android, and web notifications delivered straight to my browser on desktop or mobile.
I’ve used notifications for so many things in my projects:
- News flashes: Instantly alerting me when a breaking news keyword shows up in an API feed.
- Reminders: Pinging me to stand up during long coding sessions or to review my daily task list.
- Error alerts: Letting me know the moment a critical server job fails so I can jump in and fix it.
- Marketing: Sending targeted promotions or updates to users when they opt in.
What Is ntfy.sh?
I first stumbled upon ntfy.sh when I needed a no-frills way to send push alerts from my scripts.
ntfy.sh is a simple HTTP-based pub-sub notification service that’s completely free and open source.
With nothing more than a PUT or POST request, I can send a message from any computer and have it pop up on my phone or desktop—no sign-up, no fees, and full source code if I ever want to self-host.
How it works
What I love about ntfy.sh is its effortless setup: topics are created on the fly the moment I publish or subscribe to them.
Behind the scenes, ntfy.sh treats the topic name itself as a lightweight “password”—so I simply choose a unique topic (for example, mytopic
), send messages with curl -d "Hello" ntfy.sh/mytopic
, and open the ntfy app or web UI to subscribe to that topic.
Messages flow instantly from my script to my devices via HTTP PUT/POST, or through the ntfy CLI if I prefer.
Key features
Over time, I’ve come to rely on several powerful features that ntfy.sh supports out of the box:
-
Priorities: I can set
Priority: high
in the headers to differentiate urgencies. - Tags & Action Buttons: I tag messages and even include quick-action buttons for one-tap responses.
-
Attachments: Sending images or files is as simple as adding an
X-Image
orX-Attachment
header. - Markdown support: In the web UI, I can include formatted text, links, and emojis to make notifications richer.
With this minimal setup, ntfy.sh lets me focus on my automation logic rather than wrestling with complex notification infrastructure.
Use Cases for ntfy.sh
I’ve integrated ntfy.sh into so many parts of my workflow—here are a few examples:
System monitoring & DevOps: Whenever one of my CI/CD jobs fails or a deployment completes, I have a tiny Python script that POSTs an alert to my dev-ops
topic. The moment something goes wrong (or right!), ntfy.sh pushes a notification to my phone and browser so I can react instantly—no polling or dashboard refreshing required.
Home automation: I hooked up my temperature sensors to send messages via ntfy.sh. Now, if my attic temperature spikes, I get an instant push alert—perfect for keeping an eye on my home lab from anywhere.
Web scraping & data pipelines: In one of my Python ETL scripts, I wrap key steps in try/except blocks that POST errors or threshold-breach messages to an etl-alerts
topic. If my data pipeline ever hiccups—like a missing field or unexpected data spike—I’m notified right away to dive in and fix it.
Personal productivity: I even use ntfy.sh for reminders—sending messages to a reminders
topic whenever it’s time to stand up, review my task list, or prep for my next meeting. It’s like having a custom, lightweight to-do app that pops up wherever I am.
Sample Application: Folder Monitor with ntfy.sh
For one of my projects, I needed a way to keep an eye on a critical folder—whether files were being added, modified, moved, or deleted—without constantly refreshing a file browser.
That’s when I wrote a Folder Monitor in Python that sends real-time push notifications via ntfy.sh.
Here’s how this sample application it works, with the key code snippets for each part.
Setting up the file watcher
I leverage the watchdog
library’s Observer
and FileSystemEventHandler
to watch a directory of my choice.
Don't forget to install:
pip install requests watchdog
In my main()
function, I parse command-line arguments for the folder path, ntfy topic, file extensions filter, and whether to recurse into subdirectories.
Once validated, I instantiate my custom FileChangeHandler
and start the observer loop:
import json
import time
import argparse
import logging
import os
import sys
from datetime import datetime
import requests
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
# Set up logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)
def main():
parser = argparse.ArgumentParser(description="Monitor a folder and send ntfy.sh notifications on file changes")
parser.add_argument("--path", required=True, help="Path to the folder to monitor")
parser.add_argument("--topic", required=True, help="ntfy.sh topic for notifications")
parser.add_argument("--extensions", help="Comma-separated list of file extensions to monitor (e.g., .txt,.pdf,.docx)")
parser.add_argument("--include-directories", action="store_true", help="Include directory events in notifications")
parser.add_argument("--recursive", action="store_true", help="Watch subdirectories recursively")
args = parser.parse_args()
# Check if the path exists
if not os.path.exists(args.path):
logger.error(f"The specified path does not exist: {args.path}")
sys.exit(1)
# Process file extensions if provided
include_extensions = None
if args.extensions:
include_extensions = [ext.strip().lower() if ext.strip().startswith('.') else f'.{ext.strip().lower()}'
for ext in args.extensions.split(',')]
# Create the event handler and observer
event_handler = FileChangeHandler(
args.topic,
include_extensions=include_extensions,
exclude_directories=not args.include_directories
)
observer = Observer()
observer.schedule(event_handler, args.path, recursive=args.recursive)
observer.start()
logger.info(f"Monitoring folder: {os.path.abspath(args.path)} (recursive: {args.recursive})")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("Monitoring stopped by user")
# Send notification that monitoring has stopped
event_handler.send_notification(
"Folder monitoring stopped by user",
title="Monitoring Stopped",
priority=3, # default priority
tags="stop_sign"
)
observer.stop()
observer.join()
if __name__ == "__main__":
main()
If I hit Ctrl+C
, the script catches the KeyboardInterrupt
, stops the observer, notifies me that monitoring has stopped, and then exits cleanly.
Initializing notifications
As soon as the handler is created, I immediately send a “monitoring started” message to my ntfy.sh topic—so I know the service is up and running.
This happens right inside the FileChangeHandler.__init__()
:
class FileChangeHandler(FileSystemEventHandler):
"""Handle file system events and send notifications"""
def __init__(self, topic, include_extensions=None, exclude_directories=True):
"""
Initialize the handler
Args:
topic (str): ntfy topic to send notifications to
include_extensions (list, optional): List of file extensions to monitor (e.g. ['.txt', '.pdf'])
exclude_directories (bool): Whether to exclude directory events
"""
self.topic = topic
self.include_extensions = include_extensions
self.exclude_directories = exclude_directories
# Send a notification that monitoring has started
self.send_notification(
"Started monitoring folder for changes",
title="Folder Monitoring Started",
priority=3, # default priority
tags="rocket"
)
logger.info(f"Started monitoring. Notifications will be sent to topic: {topic}")
if include_extensions:
logger.info(f"Monitoring only these extensions: {', '.join(include_extensions)}")
Filtering events
Inside FileChangeHandler
, the should_process_event()
method ensures I only send notifications for events I care about.
I can exclude directory-only events or limit notifications to specific file extensions simply by passing flags when I launch the script:
def should_process_event(self, event):
"""Check if the event should be processed based on configuration"""
# Skip directory events if configured to do so
if self.exclude_directories and event.is_directory:
return False
# If extensions filter is set, check the file extension
if self.include_extensions and not event.is_directory:
_, ext = os.path.splitext(event.src_path)
if ext.lower() not in self.include_extensions:
return False
return True
Handling file events
Created: I grab the file’s name, size, and timestamp, log the creation, and send a notification.
def on_created(self, event):
"""Handle file/directory creation events"""
if not self.should_process_event(event):
return
path = event.src_path
filename = os.path.basename(path)
file_size = self._get_file_size(path)
logger.info(f"Created: {path}")
# Send notification for file creation
self.send_notification(
f"File created: {filename}\nLocation: {path}\nSize: {file_size}\nTime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
title="File Created",
priority=3, # default priority
tags="file_folder,new"
)
Modified: I lower the priority to avoid noise and include updated size.
def on_modified(self, event):
"""Handle file/directory modification events"""
if not self.should_process_event(event):
return
path = event.src_path
filename = os.path.basename(path)
file_size = self._get_file_size(path)
logger.info(f"Modified: {path}")
# Send notification for file modification
self.send_notification(
f"File modified: {filename}\nLocation: {path}\nSize: {file_size}\nTime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
title="File Modified",
priority=2, # low priority
tags="pencil"
)
Deleted: I bump the priority to “high” and alert with a warning tag.
def on_deleted(self, event):
"""Handle file/directory deletion events"""
if not self.should_process_event(event):
return
path = event.src_path
filename = os.path.basename(path)
logger.info(f"Deleted: {path}")
# Send notification for file deletion with high priority
self.send_notification(
f"File deleted: {filename}\nLocation: {path}\nTime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
title="File Deleted",
priority=4, # high priority
tags="wastebasket,warning"
)
Moved/Renamed: I detect whether it’s a rename or move and craft the title accordingly.
def on_moved(self, event):
"""Handle file/directory move events"""
if not self.should_process_event(event):
return
src_path = event.src_path
dest_path = event.dest_path
src_filename = os.path.basename(src_path)
dest_filename = os.path.basename(dest_path)
logger.info(f"Moved: {src_path} -> {dest_path}")
# Send notification for file move
self.send_notification(
f"File moved:\nFrom: {src_path}\nTo: {dest_path}\nTime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
title=f"File Renamed: {src_filename} → {dest_filename}" if src_path.rsplit('/', 1)[0] == dest_path.rsplit('/', 1)[0] else "File Moved",
priority=3, # default priority
tags="arrow_right"
)
Helper function for getting the file size:
def _get_file_size(self, path):
"""Get human-readable file size"""
try:
if os.path.isfile(path):
size_bytes = os.path.getsize(path)
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if size_bytes < 1024 or unit == 'TB':
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024
return "N/A (directory)"
except Exception as e:
logger.error(f"Error getting file size: {str(e)}")
return "Unknown"
Building and sending the payload
Rather than manually managing HTTP headers, I assemble a JSON payload containing keys like topic
, message
, title
, priority
, and tags
, then POST it to https://ntfy.sh
.
def send_notification(self, message, title=None, priority=None, tags=None,
click=None, attach=None, actions=None):
"""
Send a notification to ntfy.sh
Args:
message (str): The notification message
title (str, optional): Notification title
priority (str or int, optional): Priority (5=urgent, 4=high, 3=default, 2=low, 1=min)
tags (str, optional): Comma-separated list of tags/emojis
click (str, optional): URL to open when notification is clicked
attach (str, optional): URL of an attachment to include
actions (str, optional): Notification action buttons
"""
url = "https://ntfy.sh"
# Create JSON payload
payload = {
"topic": self.topic,
"message": message,
}
if title:
payload["title"] = title
if priority:
# Convert string priority to numeric priority
if isinstance(priority, str):
priority_map = {
"urgent": 5,
"high": 4,
"default": 3,
"low": 2,
"min": 1
}
if priority in priority_map:
payload["priority"] = priority_map[priority]
else:
payload["priority"] = priority
if tags:
payload["tags"] = [tags]
if click:
payload["click"] = click
if attach:
payload["attach"] = attach
if actions:
payload["actions"] = actions
try:
# Send JSON payload in the request body
response = requests.post(
url,
data=json.dumps(payload),
)
if response.status_code == 200:
logger.debug(f"Notification sent: {title}")
else:
logger.error(f"Failed to send notification: {response.status_code}")
except Exception as e:
logger.error(f"Error sending notification: {str(e)}")
Running the script
To run the script, I can pass several arguments, but the simplest format is:
python folder_monitor.py --path /path/to/monitor --topic your_unique_topic
Here, I pass a directory path and the notification topic.
Let's see some screenshots of the notifications in action, first monitoring started:
Then a trace of file changes and monitoring stopped:
And here is the logging from the script:
2025-05-13 09:52:49 - INFO - Started monitoring. Notifications will be sent to topic: DeveloperServiceTopic
2025-05-13 09:52:49 - INFO - Monitoring folder: d:\downloads (recursive: False)
2025-05-13 09:53:21 - INFO - Created: d:\downloads\New Text Document.txt
2025-05-13 09:53:23 - INFO - Moved: d:\downloads\New Text Document.txt -> d:\downloads\test_file.txt
2025-05-13 09:54:12 - INFO - Deleted: d:\downloads\test_file.txt
2025-05-13 09:54:32 - INFO - Monitoring stopped by user
You can get the source code for this script at: https://github.com/nunombispo/Folder-Monitor-with-ntfy.sh
Conclusion
I’ve found ntfy.sh to be an indispensable tool in my toolkit—here’s why:
Free, open-source, flexible, and cross-platform: I can send notifications from any script or service without worrying about costs or vendor lock-in, and it works seamlessly across Android, iOS, desktop browsers, and even CLI environments.
Best practices for reliability and security: I always choose topic names that aren’t guessable (they act like passwords), watch out for rate-limiting to avoid flooding my subscribers.
Next steps: If you’re ready to go further, consider self-hosting ntfy to keep your data entirely under your control.
Follow me on Twitter: https://twitter.com/DevAsService
Follow me on Instagram: https://www.instagram.com/devasservice/
Follow me on TikTok: https://www.tiktok.com/@devasservice
Follow me on YouTube: https://www.youtube.com/@DevAsService
Top comments (0)