If you're displaying tabular data in PyQt5, you'll probably want to let users search or filter that data at some point. Maybe you're pulling records from an SQLite database and want to add a search bar above the table so users can quickly narrow things down.
The good news is that Qt provides a built-in tool for exactly this: QSortFilterProxyModel. Despite the long name, it's straightforward to use — and yes, it works perfectly in PyQt5, not just C++.
In this tutorial, we'll build a simple table with a search bar that filters rows in real time as the user types. We'll use a custom table model backed by a Python list, but the same approach works with any Qt model, including models connected to a database.
What is QSortFilterProxyModel?
When you display data in a QTableView, you connect it to a model — an object that provides the data. A QSortFilterProxyModel sits between your data model and the view. It acts as a middleman that can sort and filter the data before it reaches the table.
The flow looks like this:
Data Model → QSortFilterProxyModel → QTableView
The proxy model doesn't change your original data. It just controls which rows (and in what order) are shown in the view. This makes it a clean and efficient way to add filtering. If you're not yet familiar with how Qt's Model/View architecture works, take a look at our Model View Architecture tutorial for a thorough introduction.
Setting Up the Table Model
Before we add filtering, we need a table with some data. We'll create a simple custom model by subclassing QAbstractTableModel. This model stores data as a nested Python list, where each inner list represents a row.
from PyQt5.QtCore import Qt, QAbstractTableModel
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
This is about as minimal as a table model gets. The data() method returns the value for each cell, and rowCount() and columnCount() tell the view how big the table is.
If you're loading data from SQLite, you could populate the data list from your query results and pass it into this model the same way.
Adding the Proxy Model and Search Bar
Now let's put it all together. We'll create a QMainWindow with a QLineEdit (the search bar) at the top, and a QTableView below it. The proxy model connects the two — whenever the text in the search bar changes, the proxy model updates its filter and the table shows only matching rows.
Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PyQt5 Edition) The hands-on guide to making apps with Python — Save time and build better with this book. Over 15K copies sold.
Here's the complete working example:
import sys
from PyQt5.QtWidgets import (
QApplication, QWidget, QTableView,
QMainWindow, QVBoxLayout, QLineEdit,
)
from PyQt5.QtCore import Qt, QSortFilterProxyModel, QAbstractTableModel
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.table = QTableView()
data = [
[4, 9, 2],
[1, "hello", 0],
[3, 5, 0],
[3, 3, "what"],
["this", 8, 9],
]
self.model = TableModel(data)
self.proxy_model = QSortFilterProxyModel()
self.proxy_model.setFilterKeyColumn(-1) # Search all columns.
self.proxy_model.setSourceModel(self.model)
self.proxy_model.sort(0, Qt.AscendingOrder)
self.table.setModel(self.proxy_model)
self.searchbar = QLineEdit()
self.searchbar.setPlaceholderText("Type to filter...")
self.searchbar.textChanged.connect(
self.proxy_model.setFilterFixedString
)
layout = QVBoxLayout()
layout.addWidget(self.searchbar)
layout.addWidget(self.table)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Run this and you'll see a table with five rows. Start typing in the search bar and the table updates instantly, showing only the rows that contain matching text.

How It Works
Let's walk through the important parts.
Creating the proxy model
self.proxy_model = QSortFilterProxyModel()
self.proxy_model.setFilterKeyColumn(-1)
self.proxy_model.setSourceModel(self.model)
We create a QSortFilterProxyModel and tell it which model to wrap using setSourceModel(). The call to setFilterKeyColumn(-1) tells the proxy to search all columns when filtering. If you only wanted to filter based on a specific column, you could pass that column's index instead (e.g., 0 for the first column, 1 for the second).
Connecting the view to the proxy
self.table.setModel(self.proxy_model)
Notice that we set the table's model to the proxy model, not our original TableModel. The view talks to the proxy, and the proxy talks to your real data model behind the scenes.
Wiring up the search bar
self.searchbar.textChanged.connect(
self.proxy_model.setFilterFixedString
)
This single line does all the work. Every time the text in the QLineEdit changes, the textChanged signal fires and passes the current text to setFilterFixedString() on the proxy model. The proxy then hides any rows that don't contain that text, and the table view updates automatically. For more on how signals and slots work in PyQt5, see our Signals, Slots & Events guide.
Choosing a Filter Mode
QSortFilterProxyModel supports several different ways to filter. We used setFilterFixedString above, which does a simple substring match. But you have other options depending on what you need:
| Slot | Behavior |
|---|---|
setFilterFixedString |
Matches rows containing the exact string (case-insensitive by default) |
setFilterWildcard |
Supports * and ? wildcard characters |
setFilterRegExp |
Full regular expression matching |
To switch modes, just connect the textChanged signal to a different slot:
# Use wildcard matching instead
self.searchbar.textChanged.connect(
self.proxy_model.setFilterWildcard
)
For most search bar use cases, setFilterFixedString is exactly what you want. Wildcard and regex matching are useful if you need to give users more control over their searches.
Controlling Case Sensitivity
By default, the filter is case-insensitive — typing "hello" will match "Hello", "HELLO", and so on. If you want case-sensitive matching, you can set it on the proxy model:
self.proxy_model.setFilterCaseSensitivity(Qt.CaseSensitive)
For a user-facing search bar, case-insensitive matching (the default) is usually the better experience.
Using This with SQLite Data
If you're working with data from an SQLite database, the approach is the same. You just need to get your data into a format your model understands. For example, you could fetch your rows and pass them in as a list of lists:
import sqlite3
connection = sqlite3.connect("my_database.db")
cursor = connection.cursor()
cursor.execute("SELECT * FROM my_table")
data = cursor.fetchall() # Returns a list of tuples.
# Convert tuples to lists (optional, but consistent with our model).
data = [list(row) for row in data]
model = TableModel(data)
From here, the proxy model and search bar work exactly the same way. The filtering all happens at the Qt level, so it doesn't matter where your data originally came from. If you're working with NumPy or Pandas data instead, see our guide on displaying tabular data with QTableView.
Summary
Adding a search/filter bar to a table in PyQt5 comes down to three things:
- Create a
QSortFilterProxyModeland set your data model as its source. - Set the table view's model to the proxy model (not the original).
- Connect a
QLineEdit'stextChangedsignal to one of the proxy model's filter slots.
The QSortFilterProxyModel handles all the heavy lifting — filtering rows, updating the view, and keeping your original data untouched. Once you've seen the pattern, you can drop it into any project that uses a QTableView. You can also use the same proxy model for sorting and filtering with more advanced controls.
Bring Your PyQt/PySide Application to Market — Specialized launch support for scientific and engineering software built using Python & Qt.