Question: I've read the following material to get a better understanding what things must be considered when working with threads in Qt:  https://doc.qt.io/qt-6/thread-basics.html https://wiki.qt.io/Threads_Events_QObjects https://www.haccks.com/posts/how-to-use-qthread-correctly-p1/ https://www.haccks.com/posts/how-to-use-qthread-correctly-p2/ https://www.kdab.com/wp-content/uploads/stories/multithreading-with-qt-1.pdf  This gave me a good inside how Qt's event loop work, under which circumstances it is blocked and how threads can be used by either subclassing QThread() or by using QObject's moveToThread(). Now, I am converting this PySide6 example which uses an endless loop in worker's run() method (and therefore blocking the event loop of the thread) to use a QTimer so that the event loop is not blocked anymore. In the example, a counter counts from one to five and prints the progress to stdout:  I have two questions:  When the QTimer() is instantiated in the constructor, I get a QObject::startTimer: Timers cannot be started from another thread error (see the whole code at the end of the post). Why? According to moveToThread() doc also all children are moved to the new thread and therefore also the timer from the constructor (and Start counting sends a signal to the run() slot which should live in the newly created thread). What's going wrong here? class WorkerQTimerBased(QObject): finished = Signal() signal_stop = Signal() def __init__(self, parent: QObject = None): super().__init__(parent) self.counter = 1 # Question 1: Why is it not allowed to create a QTimer object in the constructor? # This provokes a "QObject::startTimer: Timers cannot be started from another thread"! self.timer = QTimer() self.timer.setInterval(1000) self.timer.timeout.connect(self.timer_timeout) self.signal_stop.connect(self.stop) ...   The stop_counting() slot is invoked when clicking on Stop. The slot shall send a self.worker.signal_stop.emit() to the worker. But, the signal is received only when the print() statement is uncommented. Why? @Slot() def stop_counting(self): if self.worker and self.worker_thread: self.worker.signal_stop.emit() # print("emitted()") # Question 2: Why is the signal not emitted/received? It is emitted/received when this line is uncommented. self.worker_thread.quit() self.worker_thread.wait()    The following code is a minimal (partially) working example:  Clicking "Start Counting" works. Clicking "Stop" does not work.  The code contains a Question 1 and a Question 2 marker to denote the code part which is relevant for the respective question. import sys from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget class WorkerQTimerBased(QObject): finished = Signal() signal_stop = Signal() def __init__(self, parent: QObject = None): super().__init__(parent) self.counter = 1 # Question 1: Why is it not allowed to create a QTimer object in the constructor? # This provokes a "QObject::startTimer: Timers cannot be started from another thread"! # self.timer = QTimer() # self.timer.setInterval(1000) # self.timer.timeout.connect(self.timer_timeout) self.timer = None self.signal_stop.connect(self.stop) def run(self): self.timer = QTimer() self.timer.setInterval(1000) self.timer.timeout.connect(self.timer_timeout) self.timer.start() @Slot() def timer_timeout(self) -> None: print(self.counter) self.counter += 1 if self.counter > 5: self.stop() @Slot() def stop(self): print("signal_stop received") if self.timer: self.timer.stop() self.finished.emit() class MainWindowQTimerBased(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Counting Application (QTimer-based)") self.start_button = QPushButton("Start Counting") self.start_button.clicked.connect(self.start_counting) self.stop_button = QPushButton("Stop") self.stop_button.clicked.connect(self.stop_counting) layout = QVBoxLayout() layout.addWidget(self.start_button) layout.addWidget(self.stop_button) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget) self.worker = None self.worker_thread = None @Slot() def start_counting(self): self.start_button.setEnabled(False) self.worker_thread = QThread() self.worker = WorkerQTimerBased() self.worker.moveToThread(self.worker_thread) self.worker.finished.connect(self.counting_finished) self.worker.finished.connect(self.worker_thread.quit) self.worker_thread.started.connect(self.worker.run) # self.worker_thread.finished.connect(self.worker.deleteLater) # self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self.clean_up_threading_resources) self.worker_thread.start() @Slot() def clean_up_threading_resources(self): self.worker.deleteLater() self.worker_thread.deleteLater() self.worker = None self.worker_thread = None @Slot() def stop_counting(self): if self.worker and self.worker_thread: self.worker.signal_stop.emit() # print("emitted()") # Question 2: Why is the signal not emitted/received? self.worker_thread.quit() self.worker_thread.wait() @Slot() def counting_finished(self): print("Counting finished") self.start_button.setEnabled(True) @Slot() def closeEvent(self, event): self.stop_counting() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindowQTimerBased() window.show() sys.exit(app.exec()) 

Answer: First of all, you have to remember that PySide (as PyQt, on which it is based) is a Python binding around the Qt library, which is a compiled library written in C++: this means that the "Qt side" has absolutely no knowledge of what Python does on its own, unless it directly interacts with the library.  According to moveToThread() doc also all children are moved to the new thread and therefore also the timer from the constructor  That's true, but you're considering that aspect based on a wrong assumption, because of this: class WorkerQTimerBased(QObject): def __init__(self, parent: QObject = None): ... self.timer = QTimer()  self.timer is just a Python reference to a wrapped QTimer object. In Python terms, it's a member of a WorkerQTimerBased instance, but Qt has absolutely no way to know that relation (nor it should): for Qt, that's just an "orphan" (not parented) object that lives in the thread in which it was created; to it, the QTimer and the WorkerQTimerBased instance are two completely separated objects with absolutely no relation between them. Since self.worker = WorkerQTimerBased() is called from the main thread, this means that the above QTimer exists in that same thread (the main one, in this case). Calling moveToThread() on the worker will have absolutely no effect on the QTimer, because Qt knows absolutely nothing about that relation. Doing self.timer = QTimer() does NOT make the QTimer an actual child of WorkerQTimerBased. The only way to make a Qt object as a child of another is by creating it along with the parent in the constructor (or, eventually, using setParent()). If you want the QTimer to work within its thread (which is required in order to properly call its methods), then there are only three options:  create it with the parent in the constructor (self.timer = QTimer(self)); create it in the function that is actually run first in the thread (run() in this case); create it in the __init__ (or any different thread context), add custom signals for the worker, connect them to the QTimer slots (eg: start() or stop()) and emit those signals when required, even in "threaded" functions: Qt's thread affinity will automatically queue those signals to the timer, if its thread if it's not the same of the object emitting the signal;  Finally, keep in mind that:  "event loop" is an abstract concept: it fundamentally is something that waits for some other thing to happen (eg: "continuously check if variable x is equal to y", or "wait w time for x to be something, otherwise do z"); in Qt there can be more than one event loop, even in the same thread; a real Qt event loop uses QEventLoop, which only works with actual Qt events, and doesn't normally block the program flow of other loops (no matter if they're in the same thread or not); for instance: QDialog's exec() has its own event loop, it's still in the main thread and, theoretically, you can also have "nested" event loops (a child dialog's exec() run within a parent one, even though that practice is often discouraged); not only QThread has its own QEventLoop (which is what allows it to receive signals from other threads), but it's also the foundation of many loop-based ("exec() based") Qt objects, most importantly including the "Qt application" (QCoreApplication, QGuiApplication and QApplication); QThread, on its own, never blocks anything, unless something run within it is: for instance, an infinite and never-releasing while loop in a function connected to its started signal; one of the benefits and drawbacks of Python is the GIL, which prevents possible threading concurrency that Qt could eventually allow; this means that while a "pure Qt" program could have blocking threads that would still allow others to work, that's completely impossible with Qt Python bindings such as PySide;  I have to stress this again: PySide (and PyQt) are bindings around the Qt library. The "Qt side" knows absolutely nothing about what done on the Python side, unless it actually interacts with the binding. The limitations of Python (performance, object persistence, GIL, object referencing etc.) must be considered as possible bottlenecks, even in light of the possible the interaction/relation with the Qt library is explicitly documented (eg. "virtual" method overriding).