DEV Community

Cover image for Folder Monitor in Python with ntfy.sh Notifications
Developer Service
Developer Service

Posted on • Edited on • Originally published at developer-service.blog

1

Folder Monitor in Python with ntfy.sh Notifications

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.

Get the eBook


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 or X-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
Enter fullscreen mode Exit fullscreen mode

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() 
Enter fullscreen mode Exit fullscreen mode

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)}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
        )
Enter fullscreen mode Exit fullscreen mode

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"
        )
Enter fullscreen mode Exit fullscreen mode

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"
        )
Enter fullscreen mode Exit fullscreen mode

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"
        )
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Heroku

Amplify your impact where it matters most — building exceptional apps.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

ACI image

ACI.dev: Fully Open-source AI Agent Tool-Use Infra (Composio Alternative)

100% open-source tool-use platform (backend, dev portal, integration library, SDK/MCP) that connects your AI agents to 600+ tools with multi-tenant auth, granular permissions, and access through direct function calling or a unified MCP server.

Check out our GitHub!