QTableWidget is the ready-made table in PyQt6. You give it a size, set the header labels, and drop a QTableWidgetItem into each cell:
table = QTableWidget(3, 3)
table.setHorizontalHeaderLabels(["Name", "Role", "City"])
table.setItem(0, 0, QTableWidgetItem("Ada Lovelace"))
It keeps the data for you, which is what separates it from QTableView. For anything up to a few thousand rows that’s exactly what you want.
Screenshots below are real windows on PyQt 6.11.0 on Qt 6.11.0, Python 3.12.5.
Building a QTableWidget
Rows and columns go in the constructor, then loop over your data. Every cell needs its own item object:
import sys
from PyQt6.QtWidgets import QApplication, QTableWidget, QTableWidgetItem
app = QApplication(sys.argv)
rows = [("Ada Lovelace", "Engineer", "London"),
("Grace Hopper", "Analyst", "Leeds"),
("Alan Turing", "Designer", "Bath")]
table = QTableWidget(len(rows), 3)
table.setHorizontalHeaderLabels(["Name", "Role", "City"])
for r, record in enumerate(rows):
for c, value in enumerate(record):
table.setItem(r, c, QTableWidgetItem(value))
table.setWindowTitle("Staff")
table.resize(540, 200)
table.show()
print("rows :", table.rowCount())
print("columns:", table.columnCount())
print("cell (0,0):", table.item(0, 0).text())
sys.exit(app.exec())
Output:
rows : 3
columns: 3
cell (0,0): Ada Lovelace
QTableWidgetItem holds a string, so numbers need converting. That matters for sorting, which is covered further down.
QTableWidget selectionBehavior: selecting whole rows
By default clicking a cell selects that one cell. Most applications want the whole row instead, and that’s setSelectionBehavior:
table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
import sys
from PyQt6.QtWidgets import (QApplication, QTableWidget, QTableWidgetItem,
QAbstractItemView)
app = QApplication(sys.argv)
table = QTableWidget(3, 3)
table.setHorizontalHeaderLabels(["Name", "Role", "City"])
for r, record in enumerate([("Ada", "Engineer", "London"),
("Grace", "Analyst", "Leeds"),
("Alan", "Designer", "Bath")]):
for c, value in enumerate(record):
table.setItem(r, c, QTableWidgetItem(value))
# click any cell and the whole row highlights
table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
table.selectRow(1)
table.setWindowTitle("SelectRows")
table.resize(540, 200)
table.show()
print("behaviour :", table.selectionBehavior().name)
print("mode :", table.selectionMode().name)
print("selected :", [i.row() for i in table.selectionModel().selectedRows()])
print("row 1 name:", table.item(1, 0).text())
sys.exit(app.exec())
Output:
behaviour : SelectRows
mode : SingleSelection
selected : [1]
row 1 name: Grace
| Setting | Values | Effect |
|---|---|---|
SelectionBehavior | SelectItems, SelectRows, SelectColumns | What a click selects |
SelectionMode | SingleSelection, MultiSelection, ExtendedSelection, NoSelection | How many at once |
In PyQt6 these live in scoped enums, so the full path is QAbstractItemView.SelectionBehavior.SelectRows. PyQt5 code that says just QAbstractItemView.SelectRows will not run.
selectionModel().selectedRows() then gives you the selected rows directly, rather than a list of individual cells to deduplicate.
Reading values out of the table
item(row, column) returns the item, and .text() its contents. The trap is empty cells:
import sys
from PyQt6.QtWidgets import QApplication, QTableWidget, QTableWidgetItem
app = QApplication(sys.argv)
table = QTableWidget(2, 2)
table.setHorizontalHeaderLabels(["Item", "Qty"])
for r, record in enumerate([("Keyboard", "3"), ("Mouse", "7")]):
for c, value in enumerate(record):
table.setItem(r, c, QTableWidgetItem(value))
# one cell
print("item(1, 0):", table.item(1, 0).text())
# a whole row
row = [table.item(0, c).text() for c in range(table.columnCount())]
print("row 0 :", row)
# every row, as a list of lists
data = [[table.item(r, c).text() for c in range(table.columnCount())]
for r in range(table.rowCount())]
print("all data :", data)
# an empty cell has NO item at all, so item() returns None
table.setRowCount(3)
print("empty cell:", table.item(2, 0))
Output:
item(1, 0): Mouse
row 0 : ['Keyboard', '3']
all data : [['Keyboard', '3'], ['Mouse', '7']]
empty cell: None
item() returns None.Guard for that whenever rows can be added without every cell being populated, otherwise .text() raises an AttributeError on the None.
Adding and removing rows
insertRow takes the position, so appending means passing the current row count:
import sys
from PyQt6.QtWidgets import QApplication, QTableWidget, QTableWidgetItem
app = QApplication(sys.argv)
table = QTableWidget(2, 2)
table.setHorizontalHeaderLabels(["Item", "Qty"])
for r, record in enumerate([("Keyboard", "3"), ("Mouse", "7")]):
for c, value in enumerate(record):
table.setItem(r, c, QTableWidgetItem(value))
print("start :", table.rowCount(), "rows")
# append a row at the end
table.insertRow(table.rowCount())
table.setItem(2, 0, QTableWidgetItem("Monitor"))
table.setItem(2, 1, QTableWidgetItem("2"))
print("after insert :", table.rowCount(), "rows")
# insert at the top instead
table.insertRow(0)
table.setItem(0, 0, QTableWidgetItem("Cable"))
print("after prepend:", table.rowCount(), "rows, first is", table.item(0, 0).text())
table.removeRow(1)
print("after remove :", table.rowCount(), "rows, first is", table.item(0, 0).text())
table.setRowCount(0) # clear every row in one call
print("after reset :", table.rowCount(), "rows")
Output:
start : 2 rows
after insert : 3 rows
after prepend: 4 rows, first is Cable
after remove : 3 rows, first is Cable
after reset : 0 rows
setRowCount(0) is the quickest way to empty a table before reloading it, and it disposes of the items for you.
Stretching columns, sorting and read-only cells
Four settings turn the default table into something that looks finished:
import sys
from PyQt6.QtWidgets import (QApplication, QTableWidget, QTableWidgetItem,
QHeaderView, QAbstractItemView)
from PyQt6.QtCore import Qt
app = QApplication(sys.argv)
rows = [("Ada Lovelace", "Engineer", "London"),
("Grace Hopper", "Analyst", "Leeds"),
("Alan Turing", "Designer", "Bath")]
table = QTableWidget(len(rows), 3)
table.setHorizontalHeaderLabels(["Name", "Role", "City"])
for r, record in enumerate(rows):
for c, value in enumerate(record):
item = QTableWidgetItem(value)
if c == 2:
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
table.setItem(r, c, item)
table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
table.setAlternatingRowColors(True)
table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # read only
table.setSortingEnabled(True)
table.sortItems(0, Qt.SortOrder.AscendingOrder)
table.verticalHeader().setVisible(False)
table.setWindowTitle("Styled")
table.resize(560, 200)
table.show()
print("stretch mode :", table.horizontalHeader().sectionResizeMode(0).name)
print("editable :", table.editTriggers().value != 0)
print("sorted first :", table.item(0, 0).text())
sys.exit(app.exec())
Output:
stretch mode : Stretch
editable : False
sorted first : Ada Lovelace
setSectionResizeMode(QHeaderView.ResizeMode.Stretch)fills the width instead of leaving a gap.setAlternatingRowColors(True)makes long tables easier to scan.setEditTriggers(NoEditTriggers)makes the table read only.setSortingEnabled(True)lets the user sort by clicking a header.
One caution on sorting: items sort as text, so "10" comes before "9". Store numbers with setData(Qt.ItemDataRole.DisplayRole, value) if you need numeric order.
Turn sorting off while you fill the table, or rows will reshuffle as you insert them. It’s the same care that sorted output always needs.
More PyQt and Python GUI guides:
- PyQt6 QTreeView
- QColorDialog in PyQt6
- Draw a circle in Python turtle
- np.unique in NumPy
- List comprehension with if else
Frequently asked questions
How do I create a table in PyQt6?
QTableWidget(rows, columns), then setHorizontalHeaderLabels and a QTableWidgetItem per cell. The class is documented in the Qt QTableWidget reference.
How do I select an entire row in QTableWidget?
table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows). In PyQt6 the enum is scoped, so the full path is required.
Why does QAbstractItemView.SelectRows not work in PyQt6?
PyQt6 uses scoped enums. Write QAbstractItemView.SelectionBehavior.SelectRows instead of the PyQt5 short form.
How do I get the value of a cell?
table.item(row, column).text(). Check for None first, because a cell that was never set has no item.
How do I add a row to a QTableWidget?
table.insertRow(table.rowCount()) appends one, then set the items. insertRow(0) puts it at the top.
How do I make a QTableWidget read only?
table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) stops all editing.
What is the difference between QTableWidget and QTableView?
QTableWidget stores the data itself and suits small tables. QTableView needs a model and scales to large or remote datasets.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn