If you're dynamically adding widgets to your PyQt6 application and finding that they pop out as separate floating windows instead of appearing neatly inside your application, you're running into one of Qt's most common gotchas: widget parenting.
This problem typically shows up when widgets are added from a callback, event listener, or signal handler — but work fine when added directly in your setup code. Let's look at why this happens and how to fix it.
How Qt decides what's a window
In Qt, every widget can optionally have a parent widget. The parent determines where a widget lives visually — a widget with a parent is drawn inside that parent. A widget without a parent becomes a top-level window, floating independently on your desktop.
This is the root cause of widgets appearing outside your main window. When you create a widget and it doesn't have a parent — either because you didn't set one, or because the parent was lost somehow — Qt treats it as a standalone window.
Three things that cause parentless widgets
Here are the most common reasons widgets end up floating:
Creating widgets without a parent
# This widget has no parent — it will be a floating window
tabs = QTabWidget()
# This widget has a parent — it will appear inside parent_widget
tabs = QTabWidget(parent_widget)
When you add a widget to a layout, the layout assigns the parent automatically. But if something goes wrong between creation and layout insertion (like an exception, or the widget being shown prematurely), the widget stays parentless.
The safest approach is to pass a parent when creating widgets:
def create_new_tab(self):
wdg = QWidget()
layout = QGridLayout(wdg)
tabs = QTabWidget(wdg) # Explicitly set parent
tab1 = QWidget(tabs) # Explicitly set parent
tab2 = QWidget(tabs) # Explicitly set parent
tabs.addTab(tab1, "Start")
tabs.addTab(tab2, "Profile")
layout.addWidget(tabs)
return wdg
Accidentally recreating a widget
If you have a tab widget stored as self.w and somewhere in your code you do:
self.w = QTabWidget()
...the original tab widget is replaced. If the old widget gets garbage collected, all the tabs that had it as their parent suddenly become orphans — parentless widgets that float as independent windows.
Be careful not to reassign widget attributes unintentionally, especially in callbacks that might run multiple times.
Losing the parent reference
If you explicitly set a widget's parent to None, it becomes a top-level window:
widget.setParent(None) # This widget is now a floating window
This sometimes happens indirectly. For example, removing a widget from a layout in certain ways can clear its parent.
A clean approach to dynamic tabs
Here's a complete, working example that dynamically adds tabs without any floating-window issues. It demonstrates the correct way to set up a QTabWidget with a "+" button that adds new tabs:
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTabWidget,
QWidget, QVBoxLayout, QLabel
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Dynamic Tabs")
self.setFixedSize(600, 400)
self.tabs = QTabWidget(self)
self.tabs.currentChanged.connect(self.on_tab_changed)
# Add an initial tab
self.add_content_tab("Tab 1")
# Add the "+" tab for creating new tabs
self.tabs.addTab(QWidget(self.tabs), "+")
self.setCentralWidget(self.tabs)
def on_tab_changed(self, index):
# Check if the "+" tab was clicked
if self.tabs.tabText(index) == "+":
self.add_new_tab()
def add_new_tab(self):
# Count existing content tabs (exclude the "+" tab)
tab_count = self.tabs.count() # includes "+"
new_title = f"Tab {tab_count}"
# Insert the new tab before the "+" tab
new_tab = self.create_tab_content(new_title)
insert_index = self.tabs.count() - 1
self.tabs.insertTab(insert_index, new_tab, new_title)
# Switch to the newly created tab (avoid retriggering)
self.tabs.blockSignals(True)
self.tabs.setCurrentIndex(insert_index)
self.tabs.blockSignals(False)
def add_content_tab(self, title):
"""Add a content tab before the + tab."""
tab = self.create_tab_content(title)
# Insert before the last tab if "+" exists, otherwise just add
plus_index = None
for i in range(self.tabs.count()):
if self.tabs.tabText(i) == "+":
plus_index = i
break
if plus_index is not None:
self.tabs.insertTab(plus_index, tab, title)
else:
self.tabs.addTab(tab, title)
def create_tab_content(self, title):
"""Create the widget content for a tab."""
widget = QWidget(self.tabs) # Parent is the tab widget
layout = QVBoxLayout(widget)
label = QLabel(f"Content for {title}", widget)
layout.addWidget(label)
return widget
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
A few things to notice in this example:
- The main window inherits from
QMainWindow, andQApplicationis created separately. - Every widget is created with an explicit parent:
QWidget(self.tabs),QLabel(text, widget), etc. blockSignals(True)is used when programmatically changing the current tab to prevent thecurrentChangedsignal from firing recursively.- New tabs are inserted before the "+" tab using
insertTab, so the "+" always stays at the end.
Summary
Widget parenting is one of those things in Qt that works invisibly when everything is correct — and causes confusing visual glitches the moment something is slightly off. The good news is that once you understand the pattern, the fix is almost always the same: make sure every widget has the right parent.
If you're new to PyQt6, our guide to creating your first window covers the basics of setting up a QMainWindow, while the widgets tutorial walks through the most common widgets and how to use them correctly.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!