Why are signals defined as class variables?

Understanding why PyQt6 and PySide6 signals live on the class, not in __init__
Heads up! You've already completed this tutorial.

If you've worked with custom signals in PyQt6 or PySide6, you've probably noticed that they're always defined as class variables — sitting outside of __init__, right at the top of the class body. If you're coming from a general Python background, this might feel a little unusual. Most attributes in Python are set up inside __init__, so why are signals different?

The answer comes back to the order in which Python objects and Qt objects are created behind the scenes.

How signal definitions work

When you define a custom signal on a class like this:

python
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QMainWindow

class MyWindow(QMainWindow):

    my_custom_signal = pyqtSignal()
python
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMainWindow

class MyWindow(QMainWindow):

    my_custom_signal = Signal()

You're creating an instance of pyqtSignal (or Signal) and assigning it to the class variable my_custom_signal. At this point, nothing is happening on the Qt side. This is just a plain Python object sitting on the class.

The magic happens later, when you create an instance of MyWindow. During instantiation, the following sequence takes place:

  1. The Python object is created.
  2. Your __init__ method runs.
  3. You call super().__init__(), which triggers the parent class (QMainWindow) initialization.
  4. PyQt6 creates the underlying Qt C++ objects and hooks everything together.

During that last step, PyQt6 searches the class for any signal definitions it can find, and wires them up as real Qt signals. Because the signal was defined as a class variable, it's already there and ready to be discovered.

Why you can't define signals in __init__

You might think you could just define signals inside __init__, like you would with any other attribute:

python
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QMainWindow

class MyWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.my_custom_signal = pyqtSignal()
python
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMainWindow

class MyWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.my_custom_signal = Signal()

This won't work. When you call super().__init__(), PyQt6 goes looking for signal definitions to hook up — but at that point in the code, you haven't defined my_custom_signal yet. It doesn't exist, so PyQt6 can't find it, and it never gets wired up as a real Qt signal.

What about defining them before super()?

You could try putting the signal definition before the super().__init__() call:

python
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QMainWindow

class MyWindow(QMainWindow):

    def __init__(self):
        self.my_custom_signal = pyqtSignal()
        super().__init__()
python
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMainWindow

class MyWindow(QMainWindow):

    def __init__(self):
        self.my_custom_signal = Signal()
        super().__init__()

In theory this puts the signal in place before PyQt6 does its setup work. But this is considered bad practice -- Python developers generally expect super().__init__() to be the first line in __init__, and breaking that convention can lead to confusing, hard-to-debug problems.

Class variables keep things clean

Defining signals as class variables sidesteps all of these timing issues. The signal definitions are in place before any instance is ever created, so by the time super().__init__() runs and PyQt6 goes looking for signals, everything is already where it needs to be.

python
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QMainWindow

class MyWindow(QMainWindow):

    my_custom_signal = pyqtSignal()

    def __init__(self):
        super().__init__()
        # Signal is already hooked up and ready to use
        self.my_custom_signal.emit()
python
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMainWindow

class MyWindow(QMainWindow):

    my_custom_signal = Signal()

    def __init__(self):
        super().__init__()
        # Signal is already hooked up and ready to use
        self.my_custom_signal.emit()

This pattern is consistent, predictable, and easy to follow. You'll see it across all PyQt6 and PySide6 code, including in QObject subclasses, worker classes, and custom widgets. For a full introduction to how signals and slots work in practice, see the Signals, Slots & Events tutorial for PyQt6 or PySide6.

An Alternative: Creating a Signals Class

If you really want to define your signals in your __init__ block, there is a way to do it, without breaking conventions: define your signals on a separate signals class and then instantiate that in your __init__.

Let's take a look how that looks in practice.

python

class WindowSignals(QObject):
    my_custom_signal = pyqtSignal()

class MyWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        # We instantiate the signals here.
        self.signals = WindowSignals()
python
class WindowSignals(QObject):
    my_custom_signal = Signal()


class MyWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        # We instantiate the signals here.
        self.signals = WindowSignals()

We need our signals class to inherit from a Qt QObject base class in order for signals to be hooked up. All widget classes also inherit from QObject -- if they didn't they couldn't have signals.

Using this approach our signals are instantiated in the __init__ of our window, during setup. We can access these signals on the window, through the attribute self.signals. So for example, to emit our custom signal, we would use self.signals.my_custom_signal.emit().

This separate signals class pattern is especially useful when working with multithreading using QThreadPool, where worker runnables can't have signals directly (since QRunnable doesn't inherit from QObject) and need a dedicated signals object to communicate back to the main thread.

Summary

Custom signals are defined as class variables because of the order in which Python and Qt set things up during object creation. PyQt6 and PySide6 look for signal definitions when the Qt object is initialized (inside super().__init__()), so the signals need to already exist at that point. Putting them on the class — rather than inside __init__ — ensures they're always available when Qt needs them, without requiring any awkward workarounds in your initialization code.

If you're new to PyQt6 or PySide6 and want to understand how signals fit into the bigger picture, the creating your first window tutorial is a great place to start. You can also learn more about transmitting extra data with Qt signals for more advanced signal usage patterns.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

PyQt/PySide Office Hours 1:1 with Martin Fitzpatrick

Save yourself time and frustration. Get one on one help with your projects. Bring issues, bugs and questions about usability to architecture and maintainability, and leave with solutions.

Book Now 60 mins ($195)

Martin Fitzpatrick

Why are signals defined as class variables? was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.