When I wrote my very first test with pytest qt, I was greeted not with a helpful failure but with a scary red banner:
Fatal Python error: Aborted
At first, I thought I had broken something in my test logic. But after reducing the code, I realized the crash was happening before my test body even ran. That’s when I discovered the real culprit: mismatched Qt libraries.
My First Test
Here’s the minimal snippet I started with:
from PyQt5 import QtCore as qtc
class Sut(qtc.QObject):
sig_sig_sputnik = qtc.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
def listen(self):
pass
def test_emit_should_timeout(qtbot):
uut = Sut()
with qtbot.waitSignal(uut.sig_sig_sputnik, raising=True, timeout=200):
uut.listen()
This test should just fail with a timeout (because listen
never emits). But instead, I got a Fatal Python error deep inside pytestqt/plugin.py
’s qapp
fixture.
Why the Fatal Abort Happen
The problem wasn’t my code. It was my environment.
- My pip-installed
PyQt5==5.10.1
was compiled against Qt 5.9.x. - On my Ubuntu system, I had Qt 5.12.x libraries on disk.
- I had
LD_LIBRARY_PATH
pointing at the 5.12 runtime.
That meant:
- Python loaded PyQt5 (expecting Qt 5.9),
- Qt found a mix of 5.9 and 5.12 libs,
- and Qt aborted immediately because it doesn’t allow ABI mismatches.
That’s why pytest reported it as a Fatal Python error—Qt killed the process before the test even started.
How I Fix It
I learned the hard way that you must keep Qt consistent. Here are three approaches that worked:
All-pip (my choice)
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip setuptools wheel
pip install "PyQt5==5.12.3" pytest pytest-qt
unset LD_LIBRARY_PATH
pytest -q
All-system
Remove pip’s PyQt5, install python3-pyqt5
and friends from apt, and don’t mess with LD_LIBRARY_PATH
.
Conda
conda create -n qt-tests python=3.10 pyqt pytest pytest-qt
conda activate qt-tests
pytest -q
I also ran sanity checks:
import PyQt5.QtCore as qtc
print(qtc.QT_VERSION_STR) # Compile-time
print(qtc.qVersion()) # Runtime
Both should match. If they don’t, you’ll crash.
A Passing Test Example
Once my environment was fixed, this code passed perfectly:
from PyQt5 import QtCore as qtc
class Sut(qtc.QObject):
sig_sig_sputnik = qtc.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
def listen(self):
self.sig_sig_sputnik.emit()
def test_emit_passes(qtbot):
uut = Sut()
with qtbot.waitSignal(uut.sig_sig_sputnik, raising=True, timeout=500):
uut.listen()
Practice Leveling Up with pytest-qt
To get more comfortable, I built a few practice tests.
Signals with Arguments
class Worker(qtc.QObject):
done = qtc.pyqtSignal(int, str)
def run(self):
self.done.emit(42, "answer")
def test_signal_with_args(qtbot):
w = Worker()
with qtbot.waitSignal(w.done, raising=True) as blocker:
w.run()
assert blocker.args == [42, "answer"]
Asynchronous QTimer
class Ticker(qtc.QObject):
tick = qtc.pyqtSignal()
def start(self, delay_ms=50):
qtc.QTimer.singleShot(delay_ms, self.tick.emit)
def test_async_timer(qtbot):
t = Ticker()
with qtbot.waitSignal(t.tick, raising=True, timeout=500):
t.start(100)
Slots That Mutate State
class Counter(qtc.QObject):
tick = qtc.pyqtSignal()
valueChanged = qtc.pyqtSignal(int)
def __init__(self):
super().__init__()
self._value = 0
self.tick.connect(self._on_tick)
def _on_tick(self):
self._value += 1
self.valueChanged.emit(self._value)
def test_slot_and_state(qtbot):
c = Counter()
with qtbot.waitSignal(c.valueChanged, raising=True) as blocker:
c.tick.emit()
assert blocker.args == [1]
Negative Case with assertNotEmitted
def test_expected_timeout(qtbot):
c = Counter()
with qtbot.assertNotEmitted(c.valueChanged, timeout=100):
pass
Final Thought
In the end, the fatal error wasn’t about pytest-qt at all it was about my mismatched Qt setup. Once I aligned my environment, the crashes disappeared and my tests behaved as expected. The real takeaway is simple: keep your toolchain consistent, and pytest-qt will let you test signals and slots with confidence.