If your app looks alive but feels deadβ¦ congratulations π
Youβve probably blocked the main thread.
This post is a walkthrough of two real bugs I fixed recently:
- A UI freeze caused by blocking file I/O on the main thread
- A nasty crash where a scheduler ran before it was initialized
Both bugs were invisible during βsmall testsβ and brutally obvious in real usage.
Letβs break down what went wrong, how I fixed it, and what I learned.
π§ Problem #1: The UI Freeze from Hell
Symptoms
- Clicking Organize Files froze the entire UI
- Large file sets (50+ files) caused 5+ seconds of silence
- No progress bar movement
- Users thought the app had crashed (fair assumption)
Root Cause
I was doing exactly what every GUI app should never do:
Running file operations directly on the main GUI thread
Hereβs the original code π¬
# Preview mode - BLOCKING code
temp_file_ops = FileOperations(dest_dir, "Organized Files", dry_run=True)
temp_file_ops.start_operations()
# π¨ This loop blocks the UI
for file_path in self.selected_files:
category_path = temp_file_ops.categorize_file(file_path)
temp_file_ops.move_file(file_path, category_path)
temp_file_ops.finalize_operations()
That innocent-looking for loop?
Yeah⦠that was freezing everything.
β The Fix: Move Work Off the Main Thread
Instead of doing file I/O directly, I refactored preview mode to use the same background threading system already working for real file moves.
New Approach (Non-Blocking)
# Preview mode - NON-BLOCKING code
self.preview_file_ops = FileOperations(dest_dir, "Organized Files", dry_run=True)
self.processing_thread = ProcessingThread(
self.selected_files,
self.preview_file_ops
)
self.processing_thread.progress.connect(self.update_progress)
self.processing_thread.finished.connect(
lambda: self._on_preview_finished(dest_dir)
)
self.processing_thread.error.connect(self.on_processing_error)
self.processing_thread.start()
Why This Works
- File processing runs in a background thread
- UI stays responsive
- Progress bar updates in real time
- Errors are handled safely via signals
β¨ Chefβs kiss.
πͺ Showing the Preview After the Work Is Done
Instead of showing the preview dialog immediately, I added a new handler that fires only after the background thread finishes:
def _on_preview_finished(self, dest_dir):
self.progress_bar.setVisible(False)
if self.preview_file_ops.dry_run_manager.has_operations():
dialog = PreviewDialog(
self.preview_file_ops.dry_run_manager,
self
)
if dialog.exec() == QDialog.DialogCode.Accepted:
self._execute_organization(dest_dir)
else:
self.status_bar.showMessage("Preview cancelled")
Result:
- No frozen UI
- No half-ready preview
- Clean, predictable flow
π Before vs After (Quick Visual)
β Before (Blocking)
Main Thread
βββ Loop over files
βββ UI freezes π΅
β After (Non-Blocking)
Main Thread Background Thread
βββ UI alive π’ βββ File processing
βββ Progress bar βββ Categorization
βββ Signals βββ Dry-run tracking
𧨠Problem #2: The Scheduler That Crashed on Startup
This one was sneakier.
The Crash
Sometimes the app would crash when:
- Auto-sort was enabled before manual organization
- Scheduled jobs ran on fresh app start
- The app reopened with existing schedules
The Culprit
The scheduler was created like this:
self.scheduler = SortScheduler(None, self.categorizer)
That None?
Yeahβ¦ file_ops wasnβt initialized yet.
So when the scheduler tried to move files:
π₯ NoneType crash
π‘οΈ The Fix: Lazy Initialization + Defensive Guards
Step 1: Donβt Initialize Too Early
# Delay scheduler creation
self.scheduler = None
Step 2: Centralize Setup with _ensure_file_ops()
if self.scheduler is None:
self.scheduler = SortScheduler(self.file_ops, self.categorizer)
self.scheduler.start()
else:
self.scheduler.file_ops = self.file_ops
Now:
- Scheduler only exists when
file_opsexists - No duplicated setup logic
- No race conditions
π§― Step 3: Defense in Depth (Crash Prevention)
Even with good initialization, I added guards inside the scheduler and watcher:
if self.file_ops is None:
logging.error("FileOperations not initialized")
return False
This ensures:
- No crashes
- Errors are logged
- App degrades gracefully instead of exploding
π― Final Results
Before
β Frozen UI
β No progress feedback
β Random crashes
β Users panic
After
β
Fully responsive UI
β
Smooth progress updates
β
Safe background execution
β
Stable scheduler & watcher
β
App feels professional
π§ͺ Testing Still Matters
I manually tested:
- Small file sets
- Large file sets
- Preview ON / OFF
- Auto-sort before manual use
- Scheduled jobs on restart
Everything behaved exactly as expected π
π§ Takeaways
- Never block the GUI thread
- Reuse proven threading patterns
- Lazy initialization beats eager crashes
- Defensive checks save your future self
- If the UI freezes, users will assume the worst
If youβre building desktop apps with Python + Qt:
Threads + signals are not optional β theyβre survival tools.
Happy coding π§βπ»π₯
And may your UI never freeze again.
Top comments (0)