Reporting Progress and Cancellation from QgsTask

Make a QgsTask report honestly: a progress cadence that does not flood the event loop, cooperative cancellation checks in every loop, storing exceptions…

TL;DR: call setProgress() at a coarse cadence rather than per item, check isCanceled() inside every loop, and distinguish cancellation from failure in finished() so a user who pressed cancel does not get an error message. This page is part of the asynchronous task execution with QgsTask guide.

Complete Runnable Code

python
"""A task that reports progress honestly and cancels promptly."""
from qgis.core import QgsTask


class ProcessFeaturesTask(QgsTask):
    """Processes pre-fetched geometries, reporting progress and honouring cancel."""

    REPORT_EVERY = 200        # coarse enough not to flood the event loop

    def __init__(self, geometries: list, description: str = "Processing features"):
        super().__init__(description, QgsTask.CanCancel)
        self._geometries = list(geometries)
        self._results: list = []
        self._error = ""

    def run(self) -> bool:
        """Worker thread. Returns True on success, False on cancel or failure."""
        total = len(self._geometries)
        if total == 0:
            return True

        try:
            for index, geometry in enumerate(self._geometries):
                if self.isCanceled():
                    return False                       # cancelled, not failed
                self._results.append(self._process(geometry))

                if index % self.REPORT_EVERY == 0:
                    self.setProgress(100.0 * index / total)
        except Exception as exc:                       # never let it escape silently
            self._error = str(exc)
            return False

        self.setProgress(100.0)
        return True

    def finished(self, ok: bool) -> None:
        """Main thread. Distinguish success, cancellation and failure."""
        if ok:
            self._apply(self._results)
        elif self.isCanceled():
            pass                                       # the user asked; say nothing
        else:
            self._report(self._error or "the task failed")

    @staticmethod
    def _process(geometry):
        return geometry.area()

    def _apply(self, results) -> None:
        pass

    def _report(self, message: str) -> None:
        pass
Progress and cancellation across the thread boundary A sequence diagram showing the worker calling setProgress, the manager delivering the update to the progress widget, the user pressing cancel, and the worker observing the flag at its next check. progress widget task manager worker setProgress(37) update the bar user presses cancel cancel flag set isCanceled() at next check return False

Architecture Breakdown

setProgress() crosses a thread boundary

Progress is reported from the worker and displayed on the main thread, which means every call is marshalled through the event loop. Calling it once per feature over a million features posts a million events, and the interface spends more time updating a progress bar than the task spends working.

How often to check and report A grid of four loop shapes — per feature, per chunk, per file and per algorithm call — showing where the progress and cancellation checks belong in each. report every check cancel every a feature loop 1% of the total 1000 features a chunked loop chunk chunk a file loop file file one long algorithm via feedback via feedback

Reporting at roughly one per cent of the total is a good default: the bar moves visibly, and the overhead disappears into the work. For loops over files or chunks, once per item is already coarse enough.

isCanceled() is cooperative

Cancellation sets a flag. Nothing interrupts your code, and a run() that never checks the flag runs to completion after the user presses cancel — which they experience as a cancel button that does not work.

Check at the top of every loop iteration and at any point where a long single operation is about to start. The check is cheap; the responsiveness it buys is the difference between a task users trust and one they avoid.

Three outcomes, not two

finished(ok) receives the boolean run() returned, and treating it as success-or-error loses the distinction that matters most to a user.

What finished() should do with the result A decision tree over the three outcomes a task reports: success applies the results, cancellation cleans up quietly, and failure surfaces the recorded error. What did run() return, and why? True apply results main thread, safe False, cancelled clean up quietly no error message False, failed surface the error stored on the task

A cancelled task returns False, exactly as a failed one does. Consulting isCanceled() inside finished() separates them, so a deliberate cancellation is silent and a genuine failure is reported. Getting this wrong produces the familiar annoyance of an application that shows an error dialog because you asked it to stop.

Reporting Something More Useful Than a Percentage

A percentage says how far along the task is and nothing about what it is doing. For anything that takes minutes, a description is worth more:

python
from qgis.core import QgsMessageLog, Qgis


class ChunkedTask(QgsTask):
    """Reports both a percentage and a human description of the current stage."""

    def run(self) -> bool:
        stages = [("reading inputs", self._read),
                  ("computing", self._compute),
                  ("writing output", self._write)]

        for index, (label, step) in enumerate(stages):
            if self.isCanceled():
                return False
            self.setProgress(100.0 * index / len(stages))
            QgsMessageLog.logMessage(label, "MyPlugin", Qgis.Info)
            step()
        return True

Logging the stage name gives a user something to read when the bar has been at sixty per cent for two minutes, and it gives you something to correlate against when a run is slower than expected.

What Users Read From a Progress Bar

A progress indicator makes three implicit promises: that something is happening, that it will finish, and roughly when. A task that reports badly breaks one of them, and each break has a characteristic complaint attached.

A bar that never moves reads as a hang, and users kill the operation — often the application — well before it would have completed. A bar that jumps to ninety per cent and stops there reads as a lie, and the next time they see one they will not believe it. A bar with no cancel button reads as a trap, whatever it says.

None of those are fixed by more frequent updates. They are fixed by reporting against a total you actually know, by making the last step of a long job visible rather than lumping it into the final per cent, and by passing CanCancel and honouring it. Getting those three right matters more to how a plugin is perceived than almost anything else in its interface.

Production Best Practices

  • Report at a coarse cadence, around one per cent of the total.
  • Check isCanceled() in every loop, and before any long single operation.
  • Return False on cancel, and let finished() work out why.
  • Store the exception rather than raising it out of run(). Exceptions on a worker thread do not reach the console.
  • Set progress to 100 before returning True, so the bar completes rather than jumping away at ninety-something.
  • Pass QgsTask.CanCancel, or the user gets a progress bar with no way to stop it.

Frequently Asked Questions

Why does my progress bar not move?

Either setProgress() is never called, or the task was created without a description and does not appear in the task manager widget at all. Both are easy to check: log the values you are passing and confirm the task shows up in the QGIS task panel while it runs.

A third possibility is that the work is happening in a single call that never yields — one long Processing algorithm, for instance — in which case the progress you want comes from that algorithm’s feedback object rather than from your loop.

Can I cancel a Processing algorithm running inside a task?

Yes, by passing a QgsProcessingFeedback whose isCanceled() is wired to the task’s. The algorithm checks the feedback at its own checkpoints, so cancellation is as prompt as the algorithm allows — usually good, occasionally not.

What happens to partial results when a task is cancelled?

Whatever you do with them. The task object still holds them, and finished() can apply them, discard them or write them to a partial output. Discarding is the safe default: a user who cancels usually wants nothing to have happened.

Should run() ever raise?

No. An exception escaping run() is not reported anywhere a user will see, and depending on the version it may terminate the task silently. Catch it, store it, return False, and surface it from finished() where the main thread can show a message.

How do I show progress for a task with an unknown total?

Report indeterminate progress by simply not calling setProgress(), which leaves the bar in its busy state, and log stage descriptions instead. Inventing a fake percentage that jumps around is worse than an honest indeterminate indicator.

Does progress reporting slow the task down?

At a sensible cadence, no — the cost is one queued event per report, which is negligible next to any work worth putting on a thread. At a per-item cadence over a large dataset it absolutely does, and the symptom is a task that runs slower than the same code did synchronously.

If you are unsure, time the loop with reporting disabled and again with it enabled. A difference of more than a percent or two means the cadence is too fine.

Can several tasks report to one progress bar?

Not directly — each task gets its own entry in the task manager. For a set of related tasks, a parent task with subtasks gives the manager the structure to show, and the parent’s progress aggregates the children. Building your own combined bar in a dock widget is the alternative when the presentation matters more than the integration.