A support team asked me for a ticket window where the buttons stay in the bottom-right corner, however big the window gets. That’s the job of QSpacerItem in PyQt6: an invisible, stretchable gap you add to a layout with addSpacerItem().
Here’s what we’re building:

In this tutorial, I’ll build that window one spacer at a time, so you can see exactly what each QSpacerItem in PyQt6 changes. Then we’ll test it at two sizes, swap in the addStretch() shortcut and resize a spacer at runtime.
Before You Start: install PyQt6 and check the version
You need Python 3.9 or newer and PyQt6. Install it with pip install PyQt6, then run this check. Mine printed Python 3.12.5, PyQt6 6.11.0, Qt 6.11.0:
import sys
from PyQt6.QtCore import PYQT_VERSION_STR, QT_VERSION_STR
print(f"Python {sys.version.split()[0]}, PyQt6 {PYQT_VERSION_STR}, Qt {QT_VERSION_STR}")
Output:
Python 3.12.5, PyQt6 6.11.0, Qt 6.11.0

If you’ve never opened a PyQt6 window, start with creating a basic window in PyQt6 first. The sample ticket uses these widgets and values:
| Row | Widget | Sample value |
|---|---|---|
| Header | QLabel x2 | New support ticket / Ticket #TK-1042 |
| Customer, Email | QLineEdit | Emily Carter, emily.carter@example.com |
| Priority | QComboBox | High |
| Refund (USD) | QLineEdit | $149.99 |
| Due date | QLineEdit | Nov 12, 2026 |
| Buttons | QPushButton x3 | Help, Cancel, Submit |
A spacer takes four arguments: QSpacerItem(width, height, horizontal_policy, vertical_policy). The width and height are its preferred size; the policies decide whether it may grow.
Step 1: Build the ticket form without any spacers
We’ll start with no spacers at all, so the problem is visible. Save this as ticket_form.py:
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit, QComboBox,
QPushButton, QVBoxLayout, QHBoxLayout, QFormLayout)
def build():
window = QWidget()
window.setWindowTitle("Support Ticket")
window.resize(460, 340)
root = QVBoxLayout(window)
header = QHBoxLayout()
header.addWidget(QLabel("<b>New support ticket</b>"))
header.addWidget(QLabel("Ticket #TK-1042"))
root.addLayout(header)
form = QFormLayout()
form.addRow("Customer:", QLineEdit("Emily Carter"))
form.addRow("Email:", QLineEdit("emily.carter@example.com"))
priority = QComboBox()
priority.addItems(["Low", "Normal", "High"])
priority.setCurrentText("High")
form.addRow("Priority:", priority)
form.addRow("Refund (USD):", QLineEdit("$149.99"))
form.addRow("Due date:", QLineEdit("Nov 12, 2026"))
root.addLayout(form)
buttons = QHBoxLayout()
buttons.addWidget(QPushButton("Help"))
buttons.addWidget(QPushButton("Cancel"))
buttons.addWidget(QPushButton("Submit"))
root.addLayout(buttons)
return window
if __name__ == "__main__":
app = QApplication(sys.argv)
window = build()
window.show()
sys.exit(app.exec())
The form fields are plain QLineEdit text boxes inside a QFormLayout. I wrapped everything in build() so later test scripts can import it.
- Open Command Prompt in the folder where you saved the file.
- Run
python ticket_form.py. - Drag the bottom-right corner of the window to make it taller.

Checkpoint: the buttons are as wide as the window, and the header row swallows the extra height, so the title floats in a tall empty band.
Step 2: Push the buttons right with a horizontal QSpacerItem
A horizontal spacer needs an Expanding horizontal policy and a Minimum vertical one. Add the imports, then put one spacer after Help and one between the header labels:
from PyQt6.QtWidgets import QSpacerItem, QSizePolicy
P = QSizePolicy.Policy
header.addSpacerItem(QSpacerItem(40, 20, P.Expanding, P.Minimum))
button_gap = QSpacerItem(40, 20, P.Expanding, P.Minimum)
buttons.addSpacerItem(button_gap)
The spacer soaks up the spare width, so each QPushButton drops back to its natural size. The same idea works in any QHBoxLayout row.

Checkpoint: Help is on the left, Cancel and Submit are on the right, and the empty middle is the spacer.
Step 3: Pin the buttons to the bottom with a vertical QSpacerItem
For a vertical spacer the policies flip: Minimum across, Expanding down. Add it between the form and the button row:
push_down = QSpacerItem(20, 40, P.Minimum, P.Expanding)
root.addSpacerItem(push_down)
Order matters in a QVBoxLayout: items stack in the order you add them, so a spacer added before buttons pushes that row down.

Checkpoint: the empty space now collects above the buttons and the form is packed at the top. The header sits tight against Customer, which Step 4 fixes.
Step 4: Add a fixed-size QSpacerItem below the header
Not every gap should grow. A Fixed policy keeps the spacer at exactly its size hint, which is handy for a breathing gap under a title:
root.addSpacerItem(QSpacerItem(20, 16, P.Minimum, P.Fixed)) # fixed 16 px gap
Put that line right after root.addLayout(header). It never grows or shrinks, whatever the window size.

Checkpoint: there’s a clear gap between the header and Customer, and it stays the same when you resize.
Test the layout: resize the window and measure each spacer
Here’s the complete ticket_form.py with all three spacers. build() also returns the two spacers we’ll measure:
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit, QComboBox,
QPushButton, QVBoxLayout, QHBoxLayout, QFormLayout,
QSpacerItem, QSizePolicy)
P = QSizePolicy.Policy
def build():
window = QWidget()
window.setWindowTitle("Support Ticket")
window.resize(460, 340)
root = QVBoxLayout(window)
header = QHBoxLayout()
header.addWidget(QLabel("<b>New support ticket</b>"))
header.addSpacerItem(QSpacerItem(40, 20, P.Expanding, P.Minimum))
header.addWidget(QLabel("Ticket #TK-1042"))
root.addLayout(header)
root.addSpacerItem(QSpacerItem(20, 16, P.Minimum, P.Fixed)) # fixed 16 px gap
form = QFormLayout()
form.addRow("Customer:", QLineEdit("Emily Carter"))
form.addRow("Email:", QLineEdit("emily.carter@example.com"))
priority = QComboBox()
priority.addItems(["Low", "Normal", "High"])
priority.setCurrentText("High")
form.addRow("Priority:", priority)
form.addRow("Refund (USD):", QLineEdit("$149.99"))
form.addRow("Due date:", QLineEdit("Nov 12, 2026"))
root.addLayout(form)
push_down = QSpacerItem(20, 40, P.Minimum, P.Expanding)
root.addSpacerItem(push_down)
buttons = QHBoxLayout()
buttons.addWidget(QPushButton("Help"))
button_gap = QSpacerItem(40, 20, P.Expanding, P.Minimum)
buttons.addSpacerItem(button_gap)
buttons.addWidget(QPushButton("Cancel"))
buttons.addWidget(QPushButton("Submit"))
root.addLayout(buttons)
return window, root, push_down, button_gap
if __name__ == "__main__":
app = QApplication(sys.argv)
window = build()[0]
window.show()
sys.exit(app.exec())
I resized the same window from 460 x 340 to 700 x 480 and captured both:

Screenshots show it, but numbers prove it. This script reads each spacer’s real size with geometry():
from PyQt6.QtWidgets import QApplication
from PyQt6.QtTest import QTest
from ticket_form import build
app = QApplication([])
window, root, push_down, button_gap = build()
window.show()
for w, h in [(460, 340), (700, 480)]:
window.resize(w, h)
QTest.qWait(100) # let the layout settle
print(f"{w}x{h}: button gap {button_gap.geometry().width()} px, "
f"space above buttons {push_down.geometry().height()} px")
Output:
460x340: button gap 174 px, space above buttons 116 px
700x480: button gap 414 px, space above buttons 256 px
At 700 x 480 the button gap grows from 174 px to 414 px and the space above the buttons from 116 px to 256 px. That’s 240 px wider and 140 px taller, matching the resize exactly, so the buttons themselves didn’t change.
What’s the difference between addStretch() and QSpacerItem?
Box layouts have shortcuts that create the spacer for