PyQt Signals Slots
Deep dive · part of Python PyQt
Qt's event system uses signals (emitted by objects) and slots (callables connected to them). Connect with signal.connect(slot); signals can carry typed arguments. Custom signals are declared with pyqtSignal.
Qt's signals announce events; slots are callables connected to react. PyQt6 exposes pyqtSignal on QObject subclasses for type-safe custom events, decoupling widgets from business logic better than callback spaghetti.
Signals can carry arguments (valueChanged(int)); queued connections cross threads safely when using QThread patterns.
Production code combines this topic with logging, tests, and clear module boundaries so refactors stay safe when requirements grow.
signal.connect(slot) links emitter to handler; disconnect prevents leaks.
Lambdas work for simple slots; partial for parameterized handlers.
pyqtSignal(int) declares typed custom signals on QObject subclasses.
emit() triggers connected slots synchronously in same thread by default.
QSlider.valueChanged is a built-in signal example.
Slot decorators (@pyqtSlot) help QMetaObject introspection.
Practice explaining pyqt signals slots aloud with a concrete example from your current project so the abstraction sticks beyond copy-paste exercises.
Long operations should not run inside slots on the GUI thread—use QThread or QRunnable with signals back to UI for progress. Multiple connections to one signal invoke in connection order.
PySide6 API mirrors PyQt6 with slightly different import paths—pick one per project.
Qt.QueuedConnection delivers cross-thread emits safely onto the GUI thread event loop without manual invokeMethod.
QueuedConnection is the safe default for worker-thread emits targeting GUI slots.
Read the parent tutorial on pythondeck.com for runnable snippets, then reproduce them locally in a virtual environment with pinned dependency versions matching your deployment target.
When pairing with teammates, agree on one idiomatic pattern per concern—mixed styles in one repo slow reviews and invite subtle integration bugs during merges.
Updating GUI from worker thread without signals—crashes or glitches.
Circular signal connections causing infinite emit loops.
Collecting QObject without parent while connections live—use parent= or deleteLater.
Mixing PyQt5 and PyQt6 in one process.
Subclass QObject for non-widget objects emitting domain signals.
Name signals after past tense events (fileSaved, countChanged).
Disconnect dynamic connections when dialogs close.
Keep business logic out of slot lambdas—call service methods.
Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.
The program below demonstrates built-in signal. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Example: Built-in signal
# Run in the REPL or save as a .py file and execute with python.
from PyQt6.QtWidgets import QApplication, QSlider, QLabel, QWidget, QVBoxLayout
from PyQt6.QtCore import Qt
app = QApplication([])
w = QWidget(); v = QVBoxLayout(w)
sl = QSlider(Qt.Orientation.Horizontal); sl.setRange(0, 100)
lbl = QLabel("0")
sl.valueChanged.connect(lambda v: lbl.setText(str(v)))
v.addWidget(sl); v.addWidget(lbl)
w.show(); app.exec()
This sample walks through custom signal in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# Example: Custom signal
# Run in the REPL or save as a .py file and execute with python.
from PyQt6.QtCore import QObject, pyqtSignal
class Counter(QObject):
changed = pyqtSignal(int)
def __init__(self):
super().__init__(); self._n = 0
def inc(self):
self._n += 1
self.changed.emit(self._n)
c = Counter()
c.changed.connect(lambda n: print("now", n))
c.inc(); c.inc(); c.inc()
Here is a hands-on illustration of value slider. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.
# QWidget signals connect to slots across objects
from PyQt6.QtWidgets import QApplication, QSlider, QLabel, QWidget, QVBoxLayout # UI
from PyQt6.QtCore import Qt # enums
app = QApplication([]) # app
win = QWidget(); layout = QVBoxLayout(win) # container
slider = QSlider(Qt.Orientation.Horizontal); slider.setRange(0, 100) # control
label = QLabel("0") # display
slider.valueChanged.connect(lambda v: label.setText(str(v))) # signal->slot
layout.addWidget(slider); layout.addWidget(label) # stack
print(label.text()) # initial
The program below demonstrates custom signal. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# QObject subclasses declare pyqtSignal attributes
from PyQt6.QtCore import QObject, pyqtSignal # signals
class Bus(QObject): # emitter
ping = pyqtSignal(str) # typed payload
def emit_ping(self): # trigger
self.ping.emit("hi") # fire
bus = Bus() # instance
bus.ping.connect(lambda msg: print("got", msg)) # slot
bus.emit_ping() # prints got hi
Related deep dives on Python PyQt: