[{"data":1,"prerenderedAt":1581},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Freport-progress-and-cancel-processing-algorithm":3},{"id":4,"title":5,"body":6,"description":1570,"extension":1571,"meta":1572,"navigation":239,"path":1577,"seo":1578,"stem":1579,"__hash__":1580},"docs\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Freport-progress-and-cancel-processing-algorithm\u002Findex.md","Report Progress and Cancellation in a Processing Algorithm",{"type":7,"value":8,"toc":1557},"minimark",[9,13,26,35,176,181,203,207,443,461,464,468,609,626,630,633,784,793,872,876,879,1097,1118,1122,1127,1130,1198,1206,1290,1294,1372,1386,1390,1443,1447,1465,1469,1475,1481,1487,1499,1508,1522,1526,1553],[10,11,5],"h1",{"id":12},"report-progress-and-cancellation-in-a-processing-algorithm",[14,15,16,17,21,22,25],"p",{},"A progress bar that sits at zero for four minutes and then jumps to done is worse than no progress bar, because the user concludes the tool has hung and kills it. A Cancel button that does nothing is worse still. Both are the same object — the ",[18,19,20],"code",{},"feedback"," passed into ",[18,23,24],{},"processAlgorithm()"," — and using it properly takes about four lines.",[14,27,28,29,34],{},"This recipe belongs to ",[30,31,33],"a",{"href":32},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002F","Processing Provider Plugins",". It covers reporting progress accurately, checking for cancellation at the right frequency, logging in a way that helps rather than floods, splitting a bar across several phases, and passing feedback into algorithms you call from inside your own.",[14,36,37],{},[38,39,44,48,52,59,76,85,95,99,104,113,118,122,126,130,133,138,142,145,149,153,156,164,168,172],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 258","img","Diagram of the feedback object connecting an algorithm to the dialog progress bar, the log panel, the cancel button and the batch runner","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"One object, four responsibilities",[49,50,51],"desc",{},"The feedback object passed into the algorithm carries progress percentages to the progress bar, informational and warning text to the log panel, and reports whether the user has pressed cancel. The same object is supplied by the batch runner and the command line runner, so an algorithm written against it behaves correctly in all three contexts.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","258","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"fbArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Everything the user sees during a run comes through here",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"288","52","184","66","10","#eef7f4","#0f766e","stroke-width:2.5",[77,96,20],{"x":79,"y":97,"style":98,"fill":93,"textAnchor":83},"78","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif",[77,100,103],{"x":79,"y":101,"style":102,"fill":75,"textAnchor":83},"100","text-anchor:middle;font-size:11px;font-family:sans-serif","passed into your algorithm",[53,105],{"x":106,"y":107,"width":108,"height":109,"rx":67,"fill":110,"stroke":111,"style":112},"16","164","168","72","#fffdf7","#2563eb","stroke-width:2",[77,114,117],{"x":101,"y":115,"style":116,"fill":111,"textAnchor":83},"190","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","setProgress",[77,119,121],{"x":101,"y":120,"style":102,"fill":75,"textAnchor":83},"212","the bar, 0 to 100",[53,123],{"x":124,"y":107,"width":108,"height":109,"rx":67,"fill":110,"stroke":125,"style":112},"200","#15803d",[77,127,129],{"x":128,"y":115,"style":116,"fill":125,"textAnchor":83},"284","pushInfo",[77,131,132],{"x":128,"y":120,"style":102,"fill":75,"textAnchor":83},"the log panel",[53,134],{"x":135,"y":107,"width":136,"height":109,"rx":67,"fill":110,"stroke":137,"style":112},"384","176","#b45309",[77,139,141],{"x":140,"y":115,"style":116,"fill":137,"textAnchor":83},"472","pushWarning",[77,143,144],{"x":140,"y":120,"style":102,"fill":75,"textAnchor":83},"problems worth seeing",[53,146],{"x":147,"y":107,"width":108,"height":109,"rx":67,"fill":110,"stroke":148,"style":112},"576","#b91c1c",[77,150,152],{"x":151,"y":115,"style":116,"fill":148,"textAnchor":83},"660","isCanceled",[77,154,155],{"x":151,"y":120,"style":102,"fill":75,"textAnchor":83},"you must check it",[157,158],"line",{"x1":159,"y1":160,"x2":161,"y2":162,"stroke":75,"style":163},"330","118","120","158","stroke-width:2;marker-end:url(#fbArrow)",[157,165],{"x1":166,"y1":160,"x2":167,"y2":162,"stroke":75,"style":163},"360","290",[157,169],{"x1":170,"y1":160,"x2":171,"y2":162,"stroke":75,"style":163},"400","460",[157,173],{"x1":174,"y1":160,"x2":175,"y2":162,"stroke":75,"style":163},"430","640",[177,178,180],"h2",{"id":179},"prerequisites","Prerequisites",[182,183,184,192,200],"ul",{},[185,186,187,191],"li",{},[188,189,190],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[185,193,194,195,199],{},"An algorithm with parameters — see ",[30,196,198],{"href":197},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fadd-parameters-to-processing-algorithm-pyqgis\u002F","Add Parameters to a Processing Algorithm",".",[185,201,202],{},"A dataset large enough that progress is visible; a few thousand features is plenty.",[177,204,206],{"id":205},"the-standard-loop","The standard loop",[208,209,214],"pre",{"className":210,"code":211,"language":212,"meta":213,"style":213},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProcessingException\n\n\ndef processAlgorithm(self, parameters, context, feedback):\n    source = self.parameterAsSource(parameters, \"INPUT\", context)\n    if source is None:\n        raise QgsProcessingException(self.invalidSourceError(parameters, \"INPUT\"))\n\n    total = 100.0 \u002F source.featureCount() if source.featureCount() else 0\n\n    for current, feature in enumerate(source.getFeatures()):\n        if feedback.isCanceled():\n            break\n\n        self.process_one(feature)\n        feedback.setProgress(int(current * total))\n\n    return {\"OUTPUT\": dest_id}\n","python","",[18,215,216,234,241,246,259,282,300,320,325,353,358,376,385,391,396,405,423,428],{"__ignoreMap":213},[217,218,220,224,228,231],"span",{"class":157,"line":219},1,[217,221,223],{"class":222},"snl16","from",[217,225,227],{"class":226},"s95oV"," qgis.core ",[217,229,230],{"class":222},"import",[217,232,233],{"class":226}," QgsProcessingException\n",[217,235,237],{"class":157,"line":236},2,[217,238,240],{"emptyLinePlaceholder":239},true,"\n",[217,242,244],{"class":157,"line":243},3,[217,245,240],{"emptyLinePlaceholder":239},[217,247,249,252,256],{"class":157,"line":248},4,[217,250,251],{"class":222},"def",[217,253,255],{"class":254},"svObZ"," processAlgorithm",[217,257,258],{"class":226},"(self, parameters, context, feedback):\n",[217,260,262,265,268,272,275,279],{"class":157,"line":261},5,[217,263,264],{"class":226},"    source ",[217,266,267],{"class":222},"=",[217,269,271],{"class":270},"sDLfK"," self",[217,273,274],{"class":226},".parameterAsSource(parameters, ",[217,276,278],{"class":277},"sU2Wk","\"INPUT\"",[217,280,281],{"class":226},", context)\n",[217,283,285,288,291,294,297],{"class":157,"line":284},6,[217,286,287],{"class":222},"    if",[217,289,290],{"class":226}," source ",[217,292,293],{"class":222},"is",[217,295,296],{"class":270}," None",[217,298,299],{"class":226},":\n",[217,301,303,306,309,312,315,317],{"class":157,"line":302},7,[217,304,305],{"class":222},"        raise",[217,307,308],{"class":226}," QgsProcessingException(",[217,310,311],{"class":270},"self",[217,313,314],{"class":226},".invalidSourceError(parameters, ",[217,316,278],{"class":277},[217,318,319],{"class":226},"))\n",[217,321,323],{"class":157,"line":322},8,[217,324,240],{"emptyLinePlaceholder":239},[217,326,328,331,333,336,339,342,345,347,350],{"class":157,"line":327},9,[217,329,330],{"class":226},"    total ",[217,332,267],{"class":222},[217,334,335],{"class":270}," 100.0",[217,337,338],{"class":222}," \u002F",[217,340,341],{"class":226}," source.featureCount() ",[217,343,344],{"class":222},"if",[217,346,341],{"class":226},[217,348,349],{"class":222},"else",[217,351,352],{"class":270}," 0\n",[217,354,356],{"class":157,"line":355},10,[217,357,240],{"emptyLinePlaceholder":239},[217,359,361,364,367,370,373],{"class":157,"line":360},11,[217,362,363],{"class":222},"    for",[217,365,366],{"class":226}," current, feature ",[217,368,369],{"class":222},"in",[217,371,372],{"class":270}," enumerate",[217,374,375],{"class":226},"(source.getFeatures()):\n",[217,377,379,382],{"class":157,"line":378},12,[217,380,381],{"class":222},"        if",[217,383,384],{"class":226}," feedback.isCanceled():\n",[217,386,388],{"class":157,"line":387},13,[217,389,390],{"class":222},"            break\n",[217,392,394],{"class":157,"line":393},14,[217,395,240],{"emptyLinePlaceholder":239},[217,397,399,402],{"class":157,"line":398},15,[217,400,401],{"class":270},"        self",[217,403,404],{"class":226},".process_one(feature)\n",[217,406,408,411,414,417,420],{"class":157,"line":407},16,[217,409,410],{"class":226},"        feedback.setProgress(",[217,412,413],{"class":270},"int",[217,415,416],{"class":226},"(current ",[217,418,419],{"class":222},"*",[217,421,422],{"class":226}," total))\n",[217,424,426],{"class":157,"line":425},17,[217,427,240],{"emptyLinePlaceholder":239},[217,429,431,434,437,440],{"class":157,"line":430},18,[217,432,433],{"class":222},"    return",[217,435,436],{"class":226}," {",[217,438,439],{"class":277},"\"OUTPUT\"",[217,441,442],{"class":226},": dest_id}\n",[14,444,445,448,449,452,453,456,457,460],{},[188,446,447],{},"Breakdown:"," This is the shape every built-in algorithm uses, and there are three details in it. The factor is computed once outside the loop, guarding against a zero feature count — a division by zero here is a surprisingly common crash on an empty layer. ",[18,450,451],{},"isCanceled()"," is checked at the top of each iteration and ",[18,454,455],{},"break"," exits cleanly, which is the whole cancellation contract: the framework never interrupts your code, it only sets a flag, so an algorithm that does not check simply cannot be cancelled. ",[18,458,459],{},"setProgress()"," takes a number from 0 to 100, and passing the raw feature index instead of the percentage produces a bar that fills in the first hundred features and then sits still.",[14,462,463],{},"Breaking rather than raising on cancellation matters: a cancelled run should return whatever it has, or nothing, without an error dialog. The framework knows the run was cancelled and reports it appropriately.",[177,465,467],{"id":466},"log-usefully-not-constantly","Log usefully, not constantly",[208,469,471],{"className":210,"code":470,"language":212,"meta":213,"style":213},"    feedback.pushInfo(self.tr(f\"Processing {source.featureCount()} parcels\"))\n\n    skipped = 0\n    for current, feature in enumerate(source.getFeatures()):\n        if feedback.isCanceled():\n            break\n        if not feature.hasGeometry():\n            skipped += 1\n            continue\n        ...\n\n    if skipped:\n        feedback.pushWarning(\n            self.tr(f\"{skipped} features had no geometry and were skipped\"))\n",[18,472,473,503,507,516,528,534,538,548,559,564,569,573,580,585],{"__ignoreMap":213},[217,474,475,478,480,483,486,489,492,495,498,501],{"class":157,"line":219},[217,476,477],{"class":226},"    feedback.pushInfo(",[217,479,311],{"class":270},[217,481,482],{"class":226},".tr(",[217,484,485],{"class":222},"f",[217,487,488],{"class":277},"\"Processing ",[217,490,491],{"class":270},"{",[217,493,494],{"class":226},"source.featureCount()",[217,496,497],{"class":270},"}",[217,499,500],{"class":277}," parcels\"",[217,502,319],{"class":226},[217,504,505],{"class":157,"line":236},[217,506,240],{"emptyLinePlaceholder":239},[217,508,509,512,514],{"class":157,"line":243},[217,510,511],{"class":226},"    skipped ",[217,513,267],{"class":222},[217,515,352],{"class":270},[217,517,518,520,522,524,526],{"class":157,"line":248},[217,519,363],{"class":222},[217,521,366],{"class":226},[217,523,369],{"class":222},[217,525,372],{"class":270},[217,527,375],{"class":226},[217,529,530,532],{"class":157,"line":261},[217,531,381],{"class":222},[217,533,384],{"class":226},[217,535,536],{"class":157,"line":284},[217,537,390],{"class":222},[217,539,540,542,545],{"class":157,"line":302},[217,541,381],{"class":222},[217,543,544],{"class":222}," not",[217,546,547],{"class":226}," feature.hasGeometry():\n",[217,549,550,553,556],{"class":157,"line":322},[217,551,552],{"class":226},"            skipped ",[217,554,555],{"class":222},"+=",[217,557,558],{"class":270}," 1\n",[217,560,561],{"class":157,"line":327},[217,562,563],{"class":222},"            continue\n",[217,565,566],{"class":157,"line":355},[217,567,568],{"class":270},"        ...\n",[217,570,571],{"class":157,"line":360},[217,572,240],{"emptyLinePlaceholder":239},[217,574,575,577],{"class":157,"line":378},[217,576,287],{"class":222},[217,578,579],{"class":226}," skipped:\n",[217,581,582],{"class":157,"line":387},[217,583,584],{"class":226},"        feedback.pushWarning(\n",[217,586,587,590,592,594,597,599,602,604,607],{"class":157,"line":393},[217,588,589],{"class":270},"            self",[217,591,482],{"class":226},[217,593,485],{"class":222},[217,595,596],{"class":277},"\"",[217,598,491],{"class":270},[217,600,601],{"class":226},"skipped",[217,603,497],{"class":270},[217,605,606],{"class":277}," features had no geometry and were skipped\"",[217,608,319],{"class":226},[14,610,611,613,614,617,618,621,622,625],{},[188,612,447],{}," One line at the start saying what is about to happen, one at the end summarising anything unusual, and nothing per feature. A message inside the loop is the fastest way to make an algorithm slow — the log widget has to render every line — and it buries the two messages that matter under fifty thousand that do not. Counting exceptions and reporting the total afterwards gives the user a usable summary; ",[18,615,616],{},"pushWarning()"," styles it so it stands out, while ",[18,619,620],{},"pushInfo()"," is for ordinary narration. For genuinely fatal problems, raise ",[18,623,624],{},"QgsProcessingException"," with a message rather than logging and continuing.",[177,627,629],{"id":628},"split-progress-across-phases","Split progress across phases",[14,631,632],{},"Most real algorithms have several stages, and a single bar covering all of them is more honest when it is divided deliberately.",[208,634,636],{"className":210,"code":635,"language":212,"meta":213,"style":213},"    feedback.setProgressText(self.tr(\"Building the index\"))\n    index = QgsSpatialIndex(source.getFeatures())\n    feedback.setProgress(20)\n\n    feedback.setProgressText(self.tr(\"Matching parcels\"))\n    step = 60.0 \u002F source.featureCount()\n    for current, feature in enumerate(source.getFeatures()):\n        if feedback.isCanceled():\n            break\n        match(feature, index)\n        feedback.setProgress(20 + int(current * step))\n\n    feedback.setProgressText(self.tr(\"Writing output\"))\n    write_results(sink)\n    feedback.setProgress(100)\n",[18,637,638,652,662,673,677,690,705,717,723,727,735,754,758,771,776],{"__ignoreMap":213},[217,639,640,643,645,647,650],{"class":157,"line":219},[217,641,642],{"class":226},"    feedback.setProgressText(",[217,644,311],{"class":270},[217,646,482],{"class":226},[217,648,649],{"class":277},"\"Building the index\"",[217,651,319],{"class":226},[217,653,654,657,659],{"class":157,"line":236},[217,655,656],{"class":226},"    index ",[217,658,267],{"class":222},[217,660,661],{"class":226}," QgsSpatialIndex(source.getFeatures())\n",[217,663,664,667,670],{"class":157,"line":243},[217,665,666],{"class":226},"    feedback.setProgress(",[217,668,669],{"class":270},"20",[217,671,672],{"class":226},")\n",[217,674,675],{"class":157,"line":248},[217,676,240],{"emptyLinePlaceholder":239},[217,678,679,681,683,685,688],{"class":157,"line":261},[217,680,642],{"class":226},[217,682,311],{"class":270},[217,684,482],{"class":226},[217,686,687],{"class":277},"\"Matching parcels\"",[217,689,319],{"class":226},[217,691,692,695,697,700,702],{"class":157,"line":284},[217,693,694],{"class":226},"    step ",[217,696,267],{"class":222},[217,698,699],{"class":270}," 60.0",[217,701,338],{"class":222},[217,703,704],{"class":226}," source.featureCount()\n",[217,706,707,709,711,713,715],{"class":157,"line":302},[217,708,363],{"class":222},[217,710,366],{"class":226},[217,712,369],{"class":222},[217,714,372],{"class":270},[217,716,375],{"class":226},[217,718,719,721],{"class":157,"line":322},[217,720,381],{"class":222},[217,722,384],{"class":226},[217,724,725],{"class":157,"line":327},[217,726,390],{"class":222},[217,728,729,732],{"class":157,"line":355},[217,730,731],{"class":222},"        match",[217,733,734],{"class":226},"(feature, index)\n",[217,736,737,739,741,744,747,749,751],{"class":157,"line":360},[217,738,410],{"class":226},[217,740,669],{"class":270},[217,742,743],{"class":222}," +",[217,745,746],{"class":270}," int",[217,748,416],{"class":226},[217,750,419],{"class":222},[217,752,753],{"class":226}," step))\n",[217,755,756],{"class":157,"line":378},[217,757,240],{"emptyLinePlaceholder":239},[217,759,760,762,764,766,769],{"class":157,"line":387},[217,761,642],{"class":226},[217,763,311],{"class":270},[217,765,482],{"class":226},[217,767,768],{"class":277},"\"Writing output\"",[217,770,319],{"class":226},[217,772,773],{"class":157,"line":393},[217,774,775],{"class":226},"    write_results(sink)\n",[217,777,778,780,782],{"class":157,"line":398},[217,779,666],{"class":226},[217,781,101],{"class":270},[217,783,672],{"class":226},[14,785,786,788,789,792],{},[188,787,447],{}," Allocating a share of the bar to each phase — 20 percent for the index, 60 for the matching, 20 for writing — gives a bar that moves at a roughly constant rate instead of one that stalls at 90 percent for a minute. ",[18,790,791],{},"setProgressText()"," puts a label above the bar, and naming the current phase is often more reassuring than the percentage: \"Matching parcels\" tells the user the tool is doing something specific. Estimate the shares from a real run rather than guessing; the point is that the bar's speed should not vary wildly, not that the split is exact.",[14,794,795],{},[38,796,799,802,805,808,811,818,826,829,833,837,840,844,848,851,857,860,864,868],{"viewBox":797,"role":41,"ariaLabel":798,"xmlns":43},"0 0 760 254","Comparison of a progress bar that jumps from zero to complete against one divided into weighted phases that advances smoothly with a label naming the current phase",[45,800,801],{},"Two progress bars over the same run",[49,803,804],{},"The first bar reports nothing until the algorithm finishes, so the user cannot tell whether it is working or hung. The second divides the run into three weighted phases, twenty percent for indexing, sixty for matching and twenty for writing, and shows a label naming the current phase, so the bar advances at a steady rate throughout.",[53,806],{"x":55,"y":55,"width":56,"height":807,"fill":58},"254",[77,809,810],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"The bar's job is to tell the user it is still working",[77,812,817],{"x":813,"y":814,"style":815,"fill":148,"textAnchor":816},"150","76","text-anchor:end;font-size:11px;font-weight:bold;font-family:sans-serif","end","no reporting",[53,819],{"x":107,"y":820,"width":821,"height":822,"rx":823,"fill":110,"stroke":824,"style":825},"58","560","30","6","#59645f","stroke-width:1.5",[53,827],{"x":107,"y":820,"width":67,"height":822,"rx":823,"fill":148,"fillOpacity":828,"stroke":148,"style":825},0.35,[77,830,832],{"x":831,"y":97,"style":102,"fill":824,"textAnchor":83},"444","nothing for four minutes, then done",[77,834,836],{"x":813,"y":835,"style":815,"fill":125,"textAnchor":816},"140","weighted phases",[53,838],{"x":107,"y":839,"width":821,"height":822,"rx":823,"fill":110,"stroke":824,"style":825},"122",[53,841],{"x":107,"y":839,"width":842,"height":822,"rx":823,"fill":111,"fillOpacity":843,"stroke":111,"style":825},"112",0.3,[53,845],{"x":846,"y":839,"width":847,"height":822,"fill":93,"fillOpacity":843,"stroke":93,"style":825},"276","336",[53,849],{"x":850,"y":839,"width":842,"height":822,"rx":823,"fill":125,"fillOpacity":843,"stroke":125,"style":825},"612",[77,852,856],{"x":853,"y":854,"style":855,"fill":111,"textAnchor":83},"220","172","text-anchor:middle;font-size:10px;font-family:sans-serif","index 20%",[77,858,859],{"x":831,"y":854,"style":855,"fill":93,"textAnchor":83},"match 60%",[77,861,863],{"x":862,"y":854,"style":855,"fill":125,"textAnchor":83},"668","write 20%",[53,865],{"x":107,"y":866,"width":821,"height":867,"rx":67,"fill":92,"stroke":93,"style":112},"196","42",[77,869,871],{"x":831,"y":870,"style":102,"fill":75,"textAnchor":83},"222","setProgressText names the phase — often more reassuring than the number",[177,873,875],{"id":874},"pass-feedback-into-nested-algorithms","Pass feedback into nested algorithms",[14,877,878],{},"An algorithm that calls other algorithms should hand the feedback down, or the bar freezes while the inner one runs.",[208,880,882],{"className":210,"code":881,"language":212,"meta":213,"style":213},"import processing\nfrom qgis.core import QgsProcessingMultiStepFeedback\n\n    steps = 3\n    multi = QgsProcessingMultiStepFeedback(steps, feedback)\n\n    multi.setCurrentStep(0)\n    buffered = processing.run(\"native:buffer\", {...},\n                              context=context, feedback=multi, is_child_algorithm=True)\n\n    multi.setCurrentStep(1)\n    clipped = processing.run(\"native:clip\", {...},\n                             context=context, feedback=multi, is_child_algorithm=True)\n\n    multi.setCurrentStep(2)\n    dissolved = processing.run(\"native:dissolve\", {...},\n                               context=context, feedback=multi, is_child_algorithm=True)\n",[18,883,884,891,902,906,916,926,930,939,961,989,993,1002,1020,1043,1047,1056,1074],{"__ignoreMap":213},[217,885,886,888],{"class":157,"line":219},[217,887,230],{"class":222},[217,889,890],{"class":226}," processing\n",[217,892,893,895,897,899],{"class":157,"line":236},[217,894,223],{"class":222},[217,896,227],{"class":226},[217,898,230],{"class":222},[217,900,901],{"class":226}," QgsProcessingMultiStepFeedback\n",[217,903,904],{"class":157,"line":243},[217,905,240],{"emptyLinePlaceholder":239},[217,907,908,911,913],{"class":157,"line":248},[217,909,910],{"class":226},"    steps ",[217,912,267],{"class":222},[217,914,915],{"class":270}," 3\n",[217,917,918,921,923],{"class":157,"line":261},[217,919,920],{"class":226},"    multi ",[217,922,267],{"class":222},[217,924,925],{"class":226}," QgsProcessingMultiStepFeedback(steps, feedback)\n",[217,927,928],{"class":157,"line":284},[217,929,240],{"emptyLinePlaceholder":239},[217,931,932,935,937],{"class":157,"line":302},[217,933,934],{"class":226},"    multi.setCurrentStep(",[217,936,55],{"class":270},[217,938,672],{"class":226},[217,940,941,944,946,949,952,955,958],{"class":157,"line":322},[217,942,943],{"class":226},"    buffered ",[217,945,267],{"class":222},[217,947,948],{"class":226}," processing.run(",[217,950,951],{"class":277},"\"native:buffer\"",[217,953,954],{"class":226},", {",[217,956,957],{"class":270},"...",[217,959,960],{"class":226},"},\n",[217,962,963,967,969,972,974,976,979,982,984,987],{"class":157,"line":327},[217,964,966],{"class":965},"s9osk","                              context",[217,968,267],{"class":222},[217,970,971],{"class":226},"context, ",[217,973,20],{"class":965},[217,975,267],{"class":222},[217,977,978],{"class":226},"multi, ",[217,980,981],{"class":965},"is_child_algorithm",[217,983,267],{"class":222},[217,985,986],{"class":270},"True",[217,988,672],{"class":226},[217,990,991],{"class":157,"line":355},[217,992,240],{"emptyLinePlaceholder":239},[217,994,995,997,1000],{"class":157,"line":360},[217,996,934],{"class":226},[217,998,999],{"class":270},"1",[217,1001,672],{"class":226},[217,1003,1004,1007,1009,1011,1014,1016,1018],{"class":157,"line":378},[217,1005,1006],{"class":226},"    clipped ",[217,1008,267],{"class":222},[217,1010,948],{"class":226},[217,1012,1013],{"class":277},"\"native:clip\"",[217,1015,954],{"class":226},[217,1017,957],{"class":270},[217,1019,960],{"class":226},[217,1021,1022,1025,1027,1029,1031,1033,1035,1037,1039,1041],{"class":157,"line":387},[217,1023,1024],{"class":965},"                             context",[217,1026,267],{"class":222},[217,1028,971],{"class":226},[217,1030,20],{"class":965},[217,1032,267],{"class":222},[217,1034,978],{"class":226},[217,1036,981],{"class":965},[217,1038,267],{"class":222},[217,1040,986],{"class":270},[217,1042,672],{"class":226},[217,1044,1045],{"class":157,"line":393},[217,1046,240],{"emptyLinePlaceholder":239},[217,1048,1049,1051,1054],{"class":157,"line":398},[217,1050,934],{"class":226},[217,1052,1053],{"class":270},"2",[217,1055,672],{"class":226},[217,1057,1058,1061,1063,1065,1068,1070,1072],{"class":157,"line":407},[217,1059,1060],{"class":226},"    dissolved ",[217,1062,267],{"class":222},[217,1064,948],{"class":226},[217,1066,1067],{"class":277},"\"native:dissolve\"",[217,1069,954],{"class":226},[217,1071,957],{"class":270},[217,1073,960],{"class":226},[217,1075,1076,1079,1081,1083,1085,1087,1089,1091,1093,1095],{"class":157,"line":425},[217,1077,1078],{"class":965},"                               context",[217,1080,267],{"class":222},[217,1082,971],{"class":226},[217,1084,20],{"class":965},[217,1086,267],{"class":222},[217,1088,978],{"class":226},[217,1090,981],{"class":965},[217,1092,267],{"class":222},[217,1094,986],{"class":270},[217,1096,672],{"class":226},[14,1098,1099,1101,1102,1105,1106,1109,1110,1113,1114,199],{},[188,1100,447],{}," ",[18,1103,1104],{},"QgsProcessingMultiStepFeedback"," wraps the real feedback and maps each child algorithm's 0-to-100 onto its slice of the overall bar, so three steps each fill a third — this is exactly what the graphical modeller does internally. Passing ",[18,1107,1108],{},"context"," keeps temporary layers alive for the duration of the parent run; omitting it is why chained outputs sometimes vanish before the next step reads them. ",[18,1111,1112],{},"is_child_algorithm=True"," suppresses the child's own result handling, so intermediate outputs are not added to the map. Cancellation propagates through the wrapper without any extra work. This composition is the same one covered in ",[30,1115,1117],{"href":1116},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","Chaining Processing Algorithms",[177,1119,1121],{"id":1120},"check-cancellation-often-enough-and-not-too-often","Check cancellation often enough — and not too often",[14,1123,1124,1126],{},[18,1125,451],{}," is cheap but not free, and the right frequency depends on the work per item.",[14,1128,1129],{},"For a loop doing milliseconds of work per feature, checking every iteration is correct and costs nothing measurable. For a tight loop doing microseconds of work over millions of items, check every few thousand:",[208,1131,1133],{"className":210,"code":1132,"language":212,"meta":213,"style":213},"    for current, feature in enumerate(source.getFeatures()):\n        if current % 1000 == 0:\n            if feedback.isCanceled():\n                break\n            feedback.setProgress(int(current * total))\n        cheap_operation(feature)\n",[18,1134,1135,1147,1168,1175,1180,1193],{"__ignoreMap":213},[217,1136,1137,1139,1141,1143,1145],{"class":157,"line":219},[217,1138,363],{"class":222},[217,1140,366],{"class":226},[217,1142,369],{"class":222},[217,1144,372],{"class":270},[217,1146,375],{"class":226},[217,1148,1149,1151,1154,1157,1160,1163,1166],{"class":157,"line":236},[217,1150,381],{"class":222},[217,1152,1153],{"class":226}," current ",[217,1155,1156],{"class":222},"%",[217,1158,1159],{"class":270}," 1000",[217,1161,1162],{"class":222}," ==",[217,1164,1165],{"class":270}," 0",[217,1167,299],{"class":226},[217,1169,1170,1173],{"class":157,"line":243},[217,1171,1172],{"class":222},"            if",[217,1174,384],{"class":226},[217,1176,1177],{"class":157,"line":248},[217,1178,1179],{"class":222},"                break\n",[217,1181,1182,1185,1187,1189,1191],{"class":157,"line":261},[217,1183,1184],{"class":226},"            feedback.setProgress(",[217,1186,413],{"class":270},[217,1188,416],{"class":226},[217,1190,419],{"class":222},[217,1192,422],{"class":226},[217,1194,1195],{"class":157,"line":284},[217,1196,1197],{"class":226},"        cheap_operation(feature)\n",[14,1199,1200,1202,1203,1205],{},[188,1201,447],{}," Batching the check and the progress update together keeps the loop tight while still responding within a fraction of a second — a thousand cheap iterations is far less than the time a user takes to notice the button did nothing. What you must not do is check only between phases: an algorithm whose single long phase takes four minutes cannot be cancelled during it, and the user will kill QGIS instead, losing whatever else they had open. Where a single operation genuinely cannot be interrupted, say so with ",[18,1204,791],{}," before starting it.",[14,1207,1208],{},[38,1209,1212,1215,1218,1221,1228,1231,1234,1239,1245,1252,1256,1260,1262,1266,1270,1275,1280,1286],{"viewBox":1210,"role":41,"ariaLabel":1211,"xmlns":43},"0 0 760 244","Timeline comparing a cancel press against three checking frequencies, showing the delay before the algorithm actually stops in each case",[45,1213,1214],{},"How long Cancel takes to work",[49,1216,1217],{},"When the user presses cancel, an algorithm checking every iteration stops almost immediately. One checking every thousand cheap iterations stops within a fraction of a second, which is imperceptible. One checking only between phases may not stop for minutes, and the user kills QGIS instead, losing everything else they had open.",[53,1219],{"x":55,"y":55,"width":56,"height":1220,"fill":58},"244",[60,1222,1223],{},[63,1224,1226],{"id":1225,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"cancelArrow",[72,1227],{"d":74,"fill":148},[77,1229,1230],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"The button does nothing until your code looks at the flag",[157,1232],{"x1":813,"y1":88,"x2":813,"y2":120,"stroke":148,"style":1233},"stroke-width:2;stroke-dasharray:5 4",[77,1235,1238],{"x":813,"y":1236,"style":1237,"fill":148,"textAnchor":83},"46","text-anchor:middle;font-size:10px;font-weight:bold;font-family:sans-serif","Cancel pressed",[77,1240,1244],{"x":1241,"y":1242,"style":1243,"fill":75,"textAnchor":816},"132","86","text-anchor:end;font-size:11px;font-family:sans-serif","every iteration",[53,1246],{"x":813,"y":1247,"width":1248,"height":1249,"rx":1250,"fill":125,"fillOpacity":1251,"stroke":125,"style":112},"70","24","22","4",0.4,[77,1253,1255],{"x":115,"y":1242,"style":1254,"fill":75},"font-size:10px;font-family:sans-serif","stops at once",[77,1257,1259],{"x":1241,"y":1258,"style":1243,"fill":75,"textAnchor":816},"136","every 1000",[53,1261],{"x":813,"y":161,"width":1247,"height":1249,"rx":1250,"fill":93,"fillOpacity":828,"stroke":93,"style":112},[77,1263,1265],{"x":1264,"y":1258,"style":1254,"fill":75},"236","a fraction of a second — fine",[77,1267,1269],{"x":1241,"y":1268,"style":1243,"fill":75,"textAnchor":816},"186","between phases",[53,1271],{"x":813,"y":1272,"width":1273,"height":1249,"rx":1250,"fill":148,"fillOpacity":1274,"stroke":148,"style":112},"170","470",0.28,[77,1276,1279],{"x":1277,"y":1268,"style":1278,"fill":148},"632","font-size:10px;font-weight:bold;font-family:sans-serif","minutes",[157,1281],{"x1":1282,"y1":1283,"x2":1282,"y2":1284,"stroke":148,"style":1285},"620","204","216","stroke-width:2;marker-end:url(#cancelArrow)",[77,1287,1289],{"x":1273,"y":1288,"style":102,"fill":824,"textAnchor":83},"232","long enough that the user kills QGIS instead",[177,1291,1293],{"id":1292},"qgis-version-compatibility","QGIS version compatibility",[1295,1296,1297,1313],"table",{},[1298,1299,1300],"thead",{},[1301,1302,1303,1307,1310],"tr",{},[1304,1305,1306],"th",{},"QGIS version",[1304,1308,1309],{},"Python",[1304,1311,1312],{},"Notes",[1314,1315,1316,1337,1347,1358],"tbody",{},[1301,1317,1318,1322,1325],{},[1319,1320,1321],"td",{},"3.22 LTR",[1319,1323,1324],{},"3.9",[1319,1326,1327,1329,1330,1329,1332,1329,1334,1336],{},[18,1328,117],{},", ",[18,1331,152],{},[18,1333,129],{},[18,1335,141],{}," and multi-step feedback all present.",[1301,1338,1339,1342,1344],{},[1319,1340,1341],{},"3.28 LTR",[1319,1343,1324],{},[1319,1345,1346],{},"Identical.",[1301,1348,1349,1352,1355],{},[1319,1350,1351],{},"3.34 LTR",[1319,1353,1354],{},"3.12",[1319,1356,1357],{},"Baseline for this page.",[1301,1359,1360,1363,1365],{},[1319,1361,1362],{},"3.40 \u002F 3.44",[1319,1364,1354],{},[1319,1366,1367,1368,1371],{},"Identical; ",[18,1369,1370],{},"pushFormattedMessage"," adds styled output in newer releases.",[14,1373,1374,1375,1377,1378,1381,1382,1385],{},"The spelling is ",[18,1376,152],{}," with one \"l\" throughout the API, following the C++ source. ",[18,1379,1380],{},"isCancelled"," does not exist and will raise an ",[18,1383,1384],{},"AttributeError"," at exactly the wrong moment.",[177,1387,1389],{"id":1388},"troubleshooting","Troubleshooting",[182,1391,1392,1400,1408,1414,1422,1433],{},[185,1393,1394,1101,1397,1399],{},[188,1395,1396],{},"The bar jumps from 0 to 100.",[18,1398,459],{}," is being passed a count rather than a percentage, or is outside the loop.",[185,1401,1402,1101,1405,1407],{},[188,1403,1404],{},"Cancel does nothing.",[18,1406,451],{}," is never checked, or only checked between phases.",[185,1409,1410,1413],{},[188,1411,1412],{},"The algorithm is slower than the same code outside Processing."," Logging inside the loop. Move it out and count instead.",[185,1415,1416,1419,1420,199],{},[188,1417,1418],{},"The bar freezes during a nested algorithm."," Feedback was not passed to the child. Use ",[18,1421,1104],{},[185,1423,1424,1101,1427,1429,1430,199],{},[188,1425,1426],{},"Intermediate layers disappear between steps.",[18,1428,1108],{}," was not passed to ",[18,1431,1432],{},"processing.run()",[185,1434,1435,1440,1441,199],{},[188,1436,1437,199],{},[18,1438,1439],{},"AttributeError: isCancelled"," One \"l\". It is ",[18,1442,152],{},[177,1444,1446],{"id":1445},"conclusion","Conclusion",[14,1448,1449,1450,1452,1453,1455,1456,1458,1459,1461,1462,1464],{},"Compute the progress factor once, check ",[18,1451,451],{}," at the top of the loop and ",[18,1454,455],{}," when it is set, and call ",[18,1457,459],{}," with a percentage. Log once at the start, once at the end, and never per feature. Split the bar into weighted phases with ",[18,1460,791],{}," naming each one, and wrap nested algorithms in ",[18,1463,1104],{}," so the bar keeps moving and Cancel keeps working all the way down.",[177,1466,1468],{"id":1467},"frequently-asked-questions","Frequently Asked Questions",[14,1470,1471,1474],{},[188,1472,1473],{},"Does the framework interrupt my algorithm when Cancel is pressed?","\nNo. It sets a flag and waits for you to notice. Code that never checks cannot be cancelled.",[14,1476,1477,1480],{},[188,1478,1479],{},"Should a cancelled run raise an exception?","\nNo — break out of the loop and return. The framework reports the cancellation itself; an exception produces a misleading error.",[14,1482,1483,1486],{},[188,1484,1485],{},"Can I report progress from a background thread?","\nThe feedback object is designed for it and marshals to the interface thread safely. Do not touch any widget directly from the algorithm.",[14,1488,1489,1492,1493,1495,1496,1498],{},[188,1490,1491],{},"How do I show an indeterminate bar?","\nDo not call ",[18,1494,459],{}," at all and use ",[18,1497,791],{}," to describe the phase. A bar that never moves with a label that changes is honest; a fake percentage is not.",[14,1500,1501,1507],{},[188,1502,1503,1504,1506],{},"Does ",[18,1505,620],{}," appear in batch mode?","\nYes, in the batch log, and in the command line runner's output — which is another reason to keep it to a few meaningful lines.",[14,1509,1510,1513,1514,1517,1518,199],{},[188,1511,1512],{},"What is the difference between feedback and a QgsTask's progress?","\nThey serve the same purpose in different frameworks. An algorithm run from the toolbox is already off the interface thread; a plugin doing its own background work uses ",[18,1515,1516],{},"QgsTask",", as in ",[30,1519,1521],{"href":1520},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Frun-background-task-with-qgstask-pyqgis\u002F","Run a Background Task with QgsTask in PyQGIS",[177,1523,1525],{"id":1524},"related","Related",[182,1527,1528,1533,1537,1543,1549],{},[185,1529,1530,1532],{},[30,1531,33],{"href":32}," — the guide this recipe belongs to",[185,1534,1535],{},[30,1536,198],{"href":197},[185,1538,1539],{},[30,1540,1542],{"href":1541},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-custom-processing-algorithm-pyqgis\u002F","Write a Custom Processing Algorithm in PyQGIS",[185,1544,1545],{},[30,1546,1548],{"href":1547},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis\u002F","Show Plugin Progress and Cancellation in PyQGIS",[185,1550,1551],{},[30,1552,1117],{"href":1116},[1554,1555,1556],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":213,"searchDepth":236,"depth":236,"links":1558},[1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569],{"id":179,"depth":236,"text":180},{"id":205,"depth":236,"text":206},{"id":466,"depth":236,"text":467},{"id":628,"depth":236,"text":629},{"id":874,"depth":236,"text":875},{"id":1120,"depth":236,"text":1121},{"id":1292,"depth":236,"text":1293},{"id":1388,"depth":236,"text":1389},{"id":1445,"depth":236,"text":1446},{"id":1467,"depth":236,"text":1468},{"id":1524,"depth":236,"text":1525},"Use the feedback object properly — accurate percentages, informative log messages, responding to cancellation promptly, splitting progress across phases, and passing feedback down into nested algorithms.","md",{"slug":1573,"type":1574,"breadcrumb":1575,"datePublished":1576,"dateModified":1576},"report-progress-and-cancel-processing-algorithm","article","Progress & Cancellation","2026-08-15","\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Freport-progress-and-cancel-processing-algorithm",{"title":5,"description":1570},"qgis-plugin-development\u002Fprocessing-provider-plugins\u002Freport-progress-and-cancel-processing-algorithm\u002Findex","hJx8uw8Bco9cyCYxR6TmCVcp8scdv9ofAk4fx5JoQFs",1786789584660]