QSpacerItem in PyQt6: Build a Form with Spacers

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:

PyQt6 support ticket window where QSpacerItem pushes the ticket number right and the Help, Cancel and Submit buttons to the bottom
The finished window: three spacers keep the header split, the form on top and the buttons at the bottom.

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
Command Prompt running a check script that prints the Python, PyQt6 and Qt versions
The version check in Command Prompt, run before any layout code.

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:

RowWidgetSample value
HeaderQLabel x2New support ticket / Ticket #TK-1042
Customer, EmailQLineEditEmily Carter, emily.carter@example.com
PriorityQComboBoxHigh
Refund (USD)QLineEdit$149.99
Due dateQLineEditNov 12, 2026
ButtonsQPushButton x3Help, 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.

  1. Open Command Prompt in the folder where you saved the file.
  2. Run python ticket_form.py.
  3. Drag the bottom-right corner of the window to make it taller.
PyQt6 ticket form without spacers: the Help, Cancel and Submit buttons stretch across the full window width
No spacers: the three buttons share the full width and the header labels take the extra height.

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.

PyQt6 ticket form with a horizontal QSpacerItem: Help stays left while Cancel and Submit move to the right edge
After Step 2: Help stays left, Cancel and Submit sit on the right, and the ticket number moves to the far right.

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.

PyQt6 ticket form with a vertical QSpacerItem pushing the button row to the bottom of the window
After Step 3: the form stays at the top and the buttons sit on the bottom edge.

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.

PyQt6 ticket form with a fixed 16 pixel QSpacerItem between the header row and the form fields
After Step 4: a fixed 16 px gap separates the header from the Customer field.

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:

Before and after: the same PyQt6 ticket window at 460 by 340 and at 700 by 480, with the buttons staying bottom-right
Left: 460 x 340. Right: 700 x 480. Only the spacers grow; the buttons and fields keep their size.

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