<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Python GUIs - inotify</title><link href="https://www.pythonguis.com/" rel="alternate"/><link href="https://www.pythonguis.com/feeds/inotify.tag.atom.xml" rel="self"/><id>https://www.pythonguis.com/</id><updated>2021-05-14T09:00:00+00:00</updated><subtitle>Create GUI applications with Python and Qt</subtitle><entry><title>Fixing inotify_add_watch errors when deleting files with QFileSystemModel — Why Qt's file watcher complains about deleted directories and how to handle it</title><link href="https://www.pythonguis.com/faq/inotify-add-watch-error-when-deleting-in-qt-but-not-command-line/" rel="alternate"/><published>2021-05-14T09:00:00+00:00</published><updated>2021-05-14T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2021-05-14:/faq/inotify-add-watch-error-when-deleting-in-qt-but-not-command-line/</id><summary type="html">I have a GUI with a tree view file explorer using &lt;code&gt;QFileSystemModel&lt;/code&gt;. When I delete a project directory, I get &lt;code&gt;inotify_add_watch&lt;/code&gt; errors like &lt;code&gt;"No such file or directory"&lt;/code&gt;. The same deletion from the command line doesn't produce these errors. What's going on, and how do I fix it?</summary><content type="html">
            &lt;blockquote&gt;
&lt;p&gt;I have a GUI with a tree view file explorer using &lt;code&gt;QFileSystemModel&lt;/code&gt;. When I delete a project directory, I get &lt;code&gt;inotify_add_watch&lt;/code&gt; errors like &lt;code&gt;"No such file or directory"&lt;/code&gt;. The same deletion from the command line doesn't produce these errors. What's going on, and how do I fix it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you've built a file explorer using &lt;code&gt;QFileSystemModel&lt;/code&gt; and &lt;code&gt;QTreeView&lt;/code&gt; in PyQt6, you may have run into a confusing situation: deleting a directory works, but Qt spits out warnings about &lt;code&gt;inotify_add_watch&lt;/code&gt; failing because the path no longer exists. This doesn't happen when you delete the same directory from a terminal, so the problem clearly has something to do with how Qt watches the file system.&lt;/p&gt;
&lt;p&gt;Let's look at why this happens and how to handle it cleanly.&lt;/p&gt;
&lt;h2 id="what-is-inotify"&gt;What is inotify?&lt;/h2&gt;
&lt;p&gt;On Linux, &lt;code&gt;inotify&lt;/code&gt; is a kernel subsystem that lets applications monitor file system events &amp;mdash; things like files being created, modified, or deleted. When you use &lt;code&gt;QFileSystemModel&lt;/code&gt;, Qt sets up &lt;code&gt;inotify&lt;/code&gt; watches on directories so it can automatically update the model (and your tree view) when files change on disk.&lt;/p&gt;
&lt;p&gt;This is what makes &lt;code&gt;QFileSystemModel&lt;/code&gt; feel "live" &amp;mdash; if you create a file in a watched directory outside of your app, the tree view updates automatically.&lt;/p&gt;
&lt;h2 id="why-the-error-appears"&gt;Why the error appears&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;inotify_add_watch&lt;/code&gt; error happens because of a timing issue between your deletion code and Qt's file system watcher. Here's the typical sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your code deletes a directory (and its contents).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;inotify&lt;/code&gt; watcher notices that something changed and fires an event.&lt;/li&gt;
&lt;li&gt;Qt's internal file watcher tries to re-watch the directory or its subdirectories.&lt;/li&gt;
&lt;li&gt;The directory no longer exists, so &lt;code&gt;inotify_add_watch&lt;/code&gt; fails and Qt logs the warning.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The root cause is that &lt;code&gt;QFileSystemModel&lt;/code&gt; is still tracking those paths at the moment the deletion happens. Qt's watcher doesn't know in advance that &lt;em&gt;you&lt;/em&gt; are the one removing the directory &amp;mdash; it just sees changes and tries to keep up.&lt;/p&gt;
&lt;p&gt;When you delete from the command line, no Qt watcher is involved, so there's no conflict.&lt;/p&gt;
&lt;h2 id="the-solution-remove-the-path-from-the-model-first"&gt;The solution: remove the path from the model first&lt;/h2&gt;
&lt;p&gt;The cleanest way to avoid these warnings is to tell the &lt;code&gt;QFileSystemModel&lt;/code&gt; to stop watching the relevant paths &lt;em&gt;before&lt;/em&gt; you delete them from disk. You can do this by working with the model's root path and the &lt;code&gt;QFileSystemWatcher&lt;/code&gt; that Qt maintains internally.&lt;/p&gt;
&lt;p&gt;However, &lt;code&gt;QFileSystemModel&lt;/code&gt; doesn't directly expose its internal &lt;code&gt;QFileSystemWatcher&lt;/code&gt;. Instead, there are a couple of practical approaches you can use.&lt;/p&gt;
&lt;h3&gt;Approach 1: Use &lt;code&gt;shutil.rmtree&lt;/code&gt; with a small delay&lt;/h3&gt;
&lt;p&gt;One straightforward approach is to remove the item from the tree view's selection and then use &lt;code&gt;QTimer.singleShot&lt;/code&gt; to delay the actual file deletion slightly. This gives &lt;code&gt;QFileSystemModel&lt;/code&gt; a moment to process the change:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import shutil
from pathlib import Path

from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import (
    QApplication,
    QMainWindow,
    QTreeView,
    QVBoxLayout,
    QWidget,
    QPushButton,
    QFileSystemModel,
    QMessageBox,
)


class FileExplorer(QMainWindow):
    def __init__(self, root_path):
        super().__init__()
        self.setWindowTitle("File Explorer")
        self.resize(600, 400)

        self.model = QFileSystemModel()
        self.model.setRootPath(root_path)

        self.tree = QTreeView()
        self.tree.setModel(self.model)
        self.tree.setRootIndex(self.model.index(root_path))

        self.delete_button = QPushButton("Delete Selected")
        self.delete_button.clicked.connect(self.delete_selected)

        layout = QVBoxLayout()
        layout.addWidget(self.tree)
        layout.addWidget(self.delete_button)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def delete_selected(self):
        indexes = self.tree.selectedIndexes()
        if not indexes:
            return

        # Get the file path from the first column index.
        index = indexes[0]
        file_path = self.model.filePath(index)

        if not file_path:
            return

        reply = QMessageBox.question(
            self,
            "Confirm Delete",
            f"Delete '{file_path}'?",
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
        )

        if reply == QMessageBox.StandardButton.Yes:
            self.perform_delete(file_path)

    def perform_delete(self, file_path):
        path = Path(file_path)

        # Remove the index from the model first, so Qt stops watching it.
        index = self.model.index(file_path)
        if index.isValid():
            self.model.remove(index)

        # Use a short delay to let the model process the removal
        # before cleaning up anything left on disk.
        QTimer.singleShot(100, lambda: self.cleanup_path(path))

    def cleanup_path(self, path):
        """Remove any remaining files/directories from disk."""
        if path.exists():
            if path.is_dir():
                shutil.rmtree(path)
            else:
                path.unlink()


app = QApplication(sys.argv)

# Change this to a directory you want to browse.
root = str(Path.home())

window = FileExplorer(root)
window.show()

sys.exit(app.exec())
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;self.model.remove(index)&lt;/code&gt; call tells &lt;code&gt;QFileSystemModel&lt;/code&gt; to delete the file or directory itself. This is the preferred way to delete through the model because Qt handles the watcher cleanup internally. The &lt;code&gt;cleanup_path&lt;/code&gt; method acts as a safety net &amp;mdash; if &lt;code&gt;model.remove()&lt;/code&gt; didn't fully remove a directory tree (for example, if it contained build artifacts that made removal partial), &lt;code&gt;shutil.rmtree&lt;/code&gt; finishes the job after a brief delay.&lt;/p&gt;
&lt;h3&gt;Approach 2: Use &lt;code&gt;QFileSystemModel.remove()&lt;/code&gt; directly&lt;/h3&gt;
&lt;p&gt;If your directories are simple (no deeply nested build output), you can rely entirely on &lt;code&gt;QFileSystemModel.remove()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;def delete_selected(self):
    indexes = self.tree.selectedIndexes()
    if not indexes:
        return

    index = indexes[0]
    file_path = self.model.filePath(index)

    reply = QMessageBox.question(
        self,
        "Confirm Delete",
        f"Delete '{file_path}'?",
        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
    )

    if reply == QMessageBox.StandardButton.Yes:
        success = self.model.remove(index)
        if not success:
            QMessageBox.warning(
                self,
                "Delete Failed",
                f"Could not delete '{file_path}'. "
                "The directory may not be empty.",
            )
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;This is the simplest option. &lt;code&gt;QFileSystemModel.remove()&lt;/code&gt; returns &lt;code&gt;True&lt;/code&gt; if the deletion succeeded and &lt;code&gt;False&lt;/code&gt; if it didn't. Because the model is handling the deletion itself, it can coordinate with its internal file watcher, and you won't see the &lt;code&gt;inotify_add_watch&lt;/code&gt; errors.&lt;/p&gt;
&lt;p&gt;The limitation is that &lt;code&gt;remove()&lt;/code&gt; may fail on non-empty directories, depending on the platform and Qt version. If you're working with project directories that contain build output, compiled files, or nested subdirectories, you may need the combined approach from the first example.&lt;/p&gt;
&lt;h2 id="when-you-need-shutilrmtree"&gt;When you need &lt;code&gt;shutil.rmtree&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;If you must use &lt;code&gt;shutil.rmtree&lt;/code&gt; (because the directory tree is complex or contains files that &lt;code&gt;QFileSystemModel.remove()&lt;/code&gt; can't handle), the pattern is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clear the selection&lt;/strong&gt; in the tree view so the view doesn't try to access the deleted index.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change the model's root path&lt;/strong&gt; if the deleted directory is currently being watched as a root.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete the files&lt;/strong&gt; with &lt;code&gt;shutil.rmtree&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here's how that looks in practice:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;def perform_delete(self, file_path):
    path = Path(file_path)

    # Clear the selection to prevent the view from accessing stale indexes.
    self.tree.clearSelection()

    # If deleting the currently-rooted directory, move the root up first.
    current_root = self.model.rootPath()
    if file_path.startswith(current_root) or file_path == current_root:
        parent_dir = str(path.parent)
        self.model.setRootPath(parent_dir)
        self.tree.setRootIndex(self.model.index(parent_dir))

    # Now delete from disk.
    if path.is_dir():
        shutil.rmtree(path)
    else:
        path.unlink()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;By moving the model's root path away from the directory you're about to delete, you reduce the chance that Qt's watcher will try to re-watch paths that no longer exist.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;inotify_add_watch&lt;/code&gt; warnings appear because &lt;code&gt;QFileSystemModel&lt;/code&gt;'s internal file watcher is still tracking directories at the moment they're deleted from disk. To avoid this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prefer &lt;code&gt;QFileSystemModel.remove()&lt;/code&gt;&lt;/strong&gt; to let Qt handle both the deletion and the watcher cleanup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you need &lt;code&gt;shutil.rmtree&lt;/code&gt;&lt;/strong&gt;, clear the tree view's selection and adjust the model's root path before deleting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;QTimer.singleShot&lt;/code&gt;&lt;/strong&gt; as a safety net if you need to clean up files that the model's &lt;code&gt;remove()&lt;/code&gt; didn't catch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These warnings are cosmetic &amp;mdash; they don't usually cause crashes or data loss &amp;mdash; but handling them properly makes your application more robust and avoids confusing log output for your users (and for you during development).&lt;/p&gt;
&lt;p&gt;If you're building a file explorer interface with &lt;code&gt;QTreeView&lt;/code&gt;, you might also find our &lt;a href="https://www.pythonguis.com/faq/qtreeview-tutorial/"&gt;QTreeView tutorial&lt;/a&gt; helpful for understanding model-view patterns with tree structures. For more on Qt's model-view architecture in general, see our guide to &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-modelview-architecture/"&gt;PyQt6 ModelView architecture&lt;/a&gt;. And if you're looking to run external deletion commands or scripts alongside your GUI, take a look at our &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-qprocess-external-programs/"&gt;QProcess tutorial for PyQt6&lt;/a&gt; which covers running external programs without blocking the UI.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt6 see my book, &lt;a href="https://www.martinfitzpatrick.com/pyqt6-book/"&gt;Create GUI Applications with Python &amp; Qt6.&lt;/a&gt;&lt;/p&gt;
            </content><category term="pyqt6"/><category term="qfilesystemmodel"/><category term="qtreeview"/><category term="filesystem"/><category term="inotify"/><category term="python"/><category term="qt"/><category term="qt6"/></entry></feed>