When I minimize my PyQt application while a background thread is running,
time.sleep()calls take much longer than expected. For example, atime.sleep(1)might take several seconds. When I bring the window back to the foreground, everything returns to normal. What's causing this, and how can I fix it?
This is a common issue on macOS that can be confusing when you first encounter it. This isn't a problem with your code, the macOS operating system is deliberately slowing them down. Let's look at what's happening and what you can do about it.
Why time.sleep() takes longer when minimized
The time.sleep() function doesn't guarantee exact timing. According to the Python documentation:
The suspension time may be longer than requested by an arbitrary amount because of the scheduling of other activity in the system.
When you minimize your application, the OS may reduce the priority of your app's threads. On macOS specifically, a feature called App Nap kicks in. App Nap is designed to save energy by throttling applications that aren't visible on screen. This means your background threads get far less CPU time, and calls like time.sleep(1) can stretch out to many seconds.
On Windows, this behavior generally doesn't occur — threads continue running at their normal priority even when the window is minimized.
Setting thread priority with QThread
One approach is to raise the priority of your QThread so the OS is less likely to throttle it. You can do this with setPriority():
self._thread.start()
self._thread.setPriority(QThread.Priority.HighestPriority)
Note that setPriority() must be called after start(), since the thread needs to be running before you can change its priority.
Here are the available priority levels in PyQt6:
| Priority | Value |
|---|---|
QThread.Priority.IdlePriority |
0 |
QThread.Priority.LowestPriority |
1 |
QThread.Priority.LowPriority |
2 |
QThread.Priority.NormalPriority |
3 |
QThread.Priority.HighPriority |
4 |
QThread.Priority.HighestPriority |
5 |
QThread.Priority.TimeCriticalPriority |
6 |
QThread.Priority.InheritPriority |
7 |
However, there's a catch. As the Qt documentation notes:
The effect of the priority parameter is dependent on the operating system's scheduling policy. In particular, the priority will be ignored on systems that do not support thread priorities.
On macOS, thread priorities may have limited or no effect when App Nap is active. So while setting priority is good practice, it won't necessarily solve the problem on its own.
Disabling App Nap on macOS
The most reliable fix on macOS is to disable App Nap. You can do this system-wide by running this command in your terminal:
defaults write NSGlobalDomain NSAppSleepDisabled -bool YES
This tells macOS not to throttle any application when it's in the background. After running this command, your background threads should execute with normal timing even when the window is minimized.
To re-enable App Nap later:
defaults write NSGlobalDomain NSAppSleepDisabled -bool NO
If you'd prefer to disable App Nap only for a specific application, you can target it by bundle identifier:
defaults write com.yourcompany.yourapp NSAppSleepDisabled -bool YES
Disabling App Nap programmatically
For a more self-contained solution, you can disable App Nap from within your Python application using the subprocess module:
import subprocess
import sys
if sys.platform == "darwin":
subprocess.run(
["defaults", "write", "NSGlobalDomain", "NSAppSleepDisabled", "-bool", "YES"]
)
You could also use the pyobjc library to interact with macOS APIs directly and disable App Nap for just your process, though the terminal command is simpler for most cases.
Consider alternatives to time.sleep() for simulating I/O
If you're using time.sleep() to simulate I/O operations (like network requests), keep in mind that time.sleep() behaves differently from actual I/O in important ways. A sleep call tells the OS "I have nothing to do for X seconds," which makes it a prime candidate for throttling. Real I/O operations — network calls, file reads, database queries — are waiting for external data, and the OS handles their scheduling differently.
For simulating slow network I/O, you could use an actual HTTP request to a service like httpbin which lets you specify a response delay:
import urllib.request
def simulate_slow_io(delay_seconds):
url = f"https://httpbin.org/delay/{delay_seconds}"
urllib.request.urlopen(url)
This gives you a more realistic simulation of I/O behavior and is less likely to be affected by App Nap, since the thread is genuinely waiting on network activity rather than explicitly sleeping.
Using QTimer instead of time.sleep()
If your goal is to run something periodically rather than simulate I/O, consider using QTimer instead of sleeping in a loop. QTimer is integrated with Qt's event loop and tends to be more reliable:
from PyQt6.QtCore import QTimer
timer = QTimer()
timer.setInterval(1000) # 1 second
timer.timeout.connect(do_something)
timer.start()
QTimer runs on the main thread's event loop, so it's a different approach from running a loop in a QThread, but for periodic tasks it's often a better fit.
Complete working example
Here's the original example updated for PyQt6, with thread priority set and a note about App Nap:
import sys
import random
import time
import functools
from PyQt6.QtWidgets import (
QApplication, QWidget, QPushButton, QMainWindow, QHBoxLayout
)
from PyQt6.QtCore import QThread, QObject
def clock(func):
"""Decorator that prints how long a function call took."""
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - t0
name = func.__name__
arg_str = ", ".join(repr(arg) for arg in args) if args else ""
print(f"[{elapsed:0.5f}s] {name}({arg_str})")
return result
return clocked
@clock
def go_sleep(sleep_time):
time.sleep(sleep_time)
def go_run():
for i in range(100):
go_sleep(random.randint(1, 3))
class WorkThread(QObject):
def __init__(self):
super().__init__()
def run(self):
go_run()
class WinForm(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Thread Timeout Test")
self.button1 = QPushButton("Run")
self.button1.clicked.connect(self.on_button_click)
self._thread = QThread(self)
self.wt = WorkThread()
self.wt.moveToThread(self._thread)
layout = QHBoxLayout()
layout.addWidget(self.button1)
main_frame = QWidget()
main_frame.setLayout(layout)
self.setCentralWidget(main_frame)
def on_button_click(self):
self.button1.setText("Running")
self._thread.started.connect(self.wt.run)
self._thread.start()
# Set high priority after starting the thread.
self._thread.setPriority(QThread.Priority.HighestPriority)
if __name__ == "__main__":
app = QApplication(sys.argv)
form = WinForm()
form.show()
sys.exit(app.exec())
Try running this example, then minimize the window. Watch the console output — if you're on macOS without App Nap disabled, you'll likely see the sleep times stretching beyond what was requested. After disabling App Nap with the terminal command shown earlier, run it again to confirm the timing returns to normal.
Summary
time.sleep()is allowed to sleep longer than requested. This is by design in Python and the OS.- On macOS, App Nap aggressively throttles minimized applications, making this much worse.
- Setting
QThreadpriority withsetPriority()can help on some operating systems but may not be enough on macOS. - The most effective macOS fix is to disable App Nap via
defaults write NSGlobalDomain NSAppSleepDisabled -bool YES. - For simulating I/O, prefer actual network calls over
time.sleep(), as the OS treats them differently. - This issue does not typically occur on Windows.
Bring Your PyQt/PySide Application to Market
Specialized launch support for scientific and engineering software built using Python & Qt.