Validate Plugin Dialog Input in PyQGIS
A plugin dialog that accepts anything and fails halfway through is worse than one that refuses to start. The user has already waited, something has probably been written to disk, and the error arrives as a traceback in a log they will not read. Validating up front costs a few dozen lines and turns every one of those failures into a sentence next to the field that caused it.
This recipe belongs to Qt Designer for GIS Interfaces. It covers three layers of validation — Qt validators on the widget, live checks that gate the OK button, and a final check on accept — plus where to display the message so it is actually seen.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A dialog with a button box, ideally built from a
.uifile — see loading a .ui file at runtime. - Inputs worth validating; the layer and field combo boxes already validate their own type.
Layer one: stop it being typed
The cheapest validation prevents impossible input at the keyboard.
from qgis.PyQt.QtGui import QDoubleValidator, QRegularExpressionValidator
from qgis.PyQt.QtCore import QRegularExpression
self.distanceEdit.setValidator(QDoubleValidator(0.0, 100000.0, 3, self))
self.prefixEdit.setValidator(
QRegularExpressionValidator(QRegularExpression(r"[A-Za-z][A-Za-z0-9_]{0,15}"), self)
)
Breakdown: A validator rejects keystrokes that would make the field invalid, so a distance field cannot contain letters and a prefix cannot start with a digit. What it does not do is guarantee the final value is acceptable: QDoubleValidator allows an intermediate state such as an empty string or a lone minus sign, because the user is still typing. That is correct behaviour and it is why a validator alone is never enough.
Prefer a QgsDoubleSpinBox or QgsSpinBox over a validated line edit where the input is genuinely numeric — they enforce the range, show the units, and handle locale decimal separators, which a hand-rolled validator does not.
Layer two: gate the OK button
The main work is one method that answers "is this dialog acceptable, and if not, why".
from qgis.PyQt.QtWidgets import QDialogButtonBox
def problems(self):
"""Return a list of human-readable problems, empty when valid."""
issues = []
layer = self.layerCombo.currentLayer()
if layer is None:
issues.append("Choose an input layer.")
elif layer.featureCount() == 0:
issues.append(f"'{layer.name()}' has no features.")
if not self.fieldCombo.currentField():
issues.append("Choose a field to summarise.")
text = self.distanceEdit.text().strip()
if not text:
issues.append("Enter a buffer distance.")
elif float(text) <= 0:
issues.append("The buffer distance must be greater than zero.")
if not self.outputWidget.filePath():
issues.append("Choose an output file.")
return issues
def revalidate(self, *_):
issues = self.problems()
self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(not issues)
self.messageLabel.setText(issues[0] if issues else "")
Breakdown: Returning a list of sentences rather than a boolean is what makes the same method serve both the button state and the message. Showing only the first problem keeps the message short and leads the user through them one at a time, which reads better than a wall of complaints. *_ on revalidate lets it connect to signals with any argument count. Note the float(text) is safe only because the validator guarantees the field is numeric — without it this needs a try.
Wire it to every input's change signal, then call it once so the initial state is right:
def wire(self):
self.layerCombo.layerChanged.connect(self.revalidate)
self.fieldCombo.fieldChanged.connect(self.revalidate)
self.distanceEdit.textChanged.connect(self.revalidate)
self.outputWidget.fileChanged.connect(self.revalidate)
self.revalidate()
Breakdown: The trailing call matters as much as the connections — signals fire on change, and the dialog opens without any change having happened, so without it the OK button starts enabled on an empty form. Connecting to textChanged rather than editingFinished gives feedback as the user types, which is the difference between a dialog that feels responsive and one that feels like it is judging you after the fact.
Layer three: check again on accept
Conditions can change between the dialog opening and OK being pressed — a layer removed from another panel, a file created by another process.
from qgis.PyQt.QtWidgets import QDialog
from qgis.core import QgsProject
import os
def accept(self):
issues = self.problems()
layer = self.layerCombo.currentLayer()
if layer is not None and QgsProject.instance().mapLayer(layer.id()) is None:
issues.append("The input layer was removed from the project.")
path = self.outputWidget.filePath()
if path and os.path.exists(path) and not self.overwriteCheck.isChecked():
issues.append(f"'{os.path.basename(path)}' already exists. Tick overwrite to replace it.")
if issues:
self.iface.messageBar().pushWarning("Cannot continue", issues[0])
self.revalidate()
return
super().accept()
Breakdown: Overriding accept() rather than connecting to the OK button's clicked signal is the correct hook, because it also covers the user pressing Return. Not calling super().accept() is what keeps the dialog open — returning early leaves everything as it was, with the message explaining why. The existence check is the one that matters most in practice: silently overwriting somebody's output is a bug they will only notice much later.
A message bar inside the dialog
The QGIS message bar can be embedded in a dialog, which puts warnings inside the window the user is looking at rather than behind it.
from qgis.gui import QgsMessageBar
from qgis.core import Qgis
def setup_message_bar(self):
self.bar = QgsMessageBar(self)
self.bar.setSizePolicy(
QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed
)
self.layout().insertWidget(0, self.bar)
def warn(self, text):
self.bar.clearWidgets()
self.bar.pushMessage("", text, level=Qgis.Warning, duration=6)
Breakdown: Inserting at position 0 puts the bar at the top of the dialog's layout, matching where QGIS puts its own. clearWidgets() before pushing prevents a stack of messages accumulating when the user tries three times. The empty title argument is deliberate — inside a dialog, the context is already obvious and a title just takes a line. A duration of a few seconds is right for information; use duration=0 for something the user must act on, and clear it yourself when they do.
Validate the output path properly
Output paths cause a disproportionate share of late failures, and three checks cover almost all of them.
import os
def output_problems(self, path):
if not path:
return "Choose an output file."
directory = os.path.dirname(path) or "."
if not os.path.isdir(directory):
return f"The folder '{directory}' does not exist."
if not os.access(directory, os.W_OK):
return f"No permission to write into '{directory}'."
if os.path.exists(path) and not os.access(path, os.W_OK):
return f"'{os.path.basename(path)}' exists and is not writable."
return None
Breakdown: Checking the directory rather than the file is what catches the case that matters: a path into a folder that does not exist fails at write time, minutes into a job. os.access is advisory on some systems and can be wrong on network shares, so it is a good check and not a guarantee — the write itself must still be error-handled. Returning None for success and a sentence for failure keeps this composable with the problems() list above.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.16 LTR | 3.7 | QgsMessageBar embeddable in dialogs; Qgis.Warning level constants present. |
| 3.22 LTR | 3.9 | QRegularExpressionValidator preferred over the deprecated QRegExpValidator. |
| 3.28 LTR | 3.9 | QgsFileWidget.fileChanged signal stable. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40+ | 3.12 | Message bar levels gain a scoped enum form; flat names remain. |
Troubleshooting
- The OK button starts enabled on an empty form.
revalidate()was not called once after wiring the signals. - Validation runs but the button never disables. The button box was found by the wrong role. Use
QDialogButtonBox.Ok. - The dialog closes despite the check.
super().accept()is being called unconditionally, or the check is onclickedrather than inaccept(). float(text)raises. The field has no validator, or the validator allows an intermediate state. Guard withtry.- Messages pile up in the bar.
clearWidgets()before each push. - The output write still fails.
os.accessis advisory. Handle the write error as well as checking beforehand.
Conclusion
Put a validator on the widget where the input has a shape, gate the OK button from one problems() method that returns sentences, and re-check on accept() for anything that could have changed since. Show the first problem next to the fields, keep a message bar in the dialog for warnings, and check the output directory rather than the output file.
Frequently Asked Questions
Should I disable the OK button or show an error on click? Disable it, and say why. A disabled button with no explanation is worse than either — the label next to the fields is what makes disabling acceptable.
How do I validate an expression field?QgsExpressionLineEdit validates as you type and exposes isValidExpression(). Combine it with a check that the expression's referenced fields exist on the chosen layer, which it cannot know.
Where should long-running validation go? Nowhere in the dialog. If checking validity is expensive — counting features across a network layer, for instance — do the cheap checks live and the expensive one after accept, reporting through the message bar and a task.
Do translated messages need anything special?
Wrap each string in self.tr() so Qt Linguist picks them up — see translating a QGIS plugin with Qt Linguist.