I have a GUI with a tree view file explorer using
QFileSystemModel. When I delete a project directory, I getinotify_add_watcherrors like"No such file or directory". The same deletion from the command line doesn't produce these errors. What's going on, and how do I fix it?
If you've built a file explorer using QFileSystemModel and QTreeView in PyQt6, you may have run into a confusing situation: deleting a directory works, but Qt spits out warnings about inotify_add_watch 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.
Let's look at why this happens and how to handle it cleanly.
What is inotify?
On Linux, inotify is a kernel subsystem that lets applications monitor file system events — things like files being created, modified, or deleted. When you use QFileSystemModel, Qt sets up inotify watches on directories so it can automatically update the model (and your tree view) when files change on disk.
This is what makes QFileSystemModel feel "live" — if you create a file in a watched directory outside of your app, the tree view updates automatically.
Why the error appears
The inotify_add_watch error happens because of a timing issue between your deletion code and Qt's file system watcher. Here's the typical sequence:
- Your code deletes a directory (and its contents).
- The
inotifywatcher notices that something changed and fires an event. - Qt's internal file watcher tries to re-watch the directory or its subdirectories.
- The directory no longer exists, so
inotify_add_watchfails and Qt logs the warning.
The root cause is that QFileSystemModel is still tracking those paths at the moment the deletion happens. Qt's watcher doesn't know in advance that you are the one removing the directory — it just sees changes and tries to keep up.
When you delete from the command line, no Qt watcher is involved, so there's no conflict.
The solution: remove the path from the model first
The cleanest way to avoid these warnings is to tell the QFileSystemModel to stop watching the relevant paths before you delete them from disk. You can do this by working with the model's root path and the QFileSystemWatcher that Qt maintains internally.
However, QFileSystemModel doesn't directly expose its internal QFileSystemWatcher. Instead, there are a couple of practical approaches you can use.
Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.
Approach 1: Use shutil.rmtree with a small delay
One straightforward approach is to remove the item from the tree view's selection and then use QTimer.singleShot to delay the actual file deletion slightly. This gives QFileSystemModel a moment to process the change:
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())
The self.model.remove(index) call tells QFileSystemModel 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 cleanup_path method acts as a safety net — if model.remove() didn't fully remove a directory tree (for example, if it contained build artifacts that made removal partial), shutil.rmtree finishes the job after a brief delay.
Approach 2: Use QFileSystemModel.remove() directly
If your directories are simple (no deeply nested build output), you can rely entirely on QFileSystemModel.remove():
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.",
)
This is the simplest option. QFileSystemModel.remove() returns True if the deletion succeeded and False 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 inotify_add_watch errors.
The limitation is that remove() 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.
When you need shutil.rmtree
If you must use shutil.rmtree (because the directory tree is complex or contains files that QFileSystemModel.remove() can't handle), the pattern is:
- Clear the selection in the tree view so the view doesn't try to access the deleted index.
- Change the model's root path if the deleted directory is currently being watched as a root.
- Delete the files with
shutil.rmtree.
Here's how that looks in practice:
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()
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.
Summary
The inotify_add_watch warnings appear because QFileSystemModel's internal file watcher is still tracking directories at the moment they're deleted from disk. To avoid this:
- Prefer
QFileSystemModel.remove()to let Qt handle both the deletion and the watcher cleanup. - If you need
shutil.rmtree, clear the tree view's selection and adjust the model's root path before deleting. - Use
QTimer.singleShotas a safety net if you need to clean up files that the model'sremove()didn't catch.
These warnings are cosmetic — they don't usually cause crashes or data loss — but handling them properly makes your application more robust and avoids confusing log output for your users (and for you during development).
If you're building a file explorer interface with QTreeView, you might also find our QTreeView tutorial helpful for understanding model-view patterns with tree structures. For more on Qt's model-view architecture in general, see our guide to PyQt6 ModelView architecture. And if you're looking to run external deletion commands or scripts alongside your GUI, take a look at our QProcess tutorial for PyQt6 which covers running external programs without blocking the UI.