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.

Three layers, each catching what the previous cannotA Qt validator on the widget prevents impossible input from being typed at all. Live validation runs on every change and enables or disables the OK button with an explanation. A final check on accept catches conditions that changed after the dialog opened, such as a layer being removed or a file appearing.Each layer catches what the one before it cannot1 · widget validatorletters in a number fieldsimply cannot be typedno message neededcheapest, narrowest2 · live validationruns on every changeenables or disables OKexplains why, inlinewhere most of it lives3 · on acceptthe layer was removedthe file now existsrefuse to closecatches raceswhat none of them replacehandling the failure when the work itself goes wrong

Prerequisites

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.

Where the message goes decides whether it is readA message next to the field the user is looking at is read immediately. A message in the QGIS message bar is seen but requires a glance away from the dialog. A modal message box interrupts and is read, but is disruptive when the problem is a field the user has not filled in yet.Put the message where the eyes already arenext to the fielddistance: (empty)Enter a buffer distance.read immediatelyno interruptionuse for live validationthe message bar⚠ Cannot continueabove the canvasseen, but a glance awaydismisses itselfuse for results and warningsa modal boxOverwrite existing file?Yes · Noalways readalways interruptsuse only for decisionsthat need an answer

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 versionPythonNotes
3.16 LTR3.7QgsMessageBar embeddable in dialogs; Qgis.Warning level constants present.
3.22 LTR3.9QRegularExpressionValidator preferred over the deprecated QRegExpValidator.
3.28 LTR3.9QgsFileWidget.fileChanged signal stable.
3.34 LTR3.12Baseline for this page.
3.40+3.12Message 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 on clicked rather than in accept().
  • float(text) raises. The field has no validator, or the validator allows an intermediate state. Guard with try.
  • Messages pile up in the bar. clearWidgets() before each push.
  • The output write still fails. os.access is 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.