[{"data":1,"prerenderedAt":1326},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-core-gui-analysis-modules-explained":3},{"id":4,"title":5,"body":6,"description":1315,"extension":1316,"meta":1317,"navigation":435,"path":1322,"seo":1323,"stem":1324,"__hash__":1325},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-core-gui-analysis-modules-explained\u002Findex.md","QGIS Core, GUI and Analysis Modules Explained",{"type":7,"value":8,"toc":1301},"minimark",[9,13,17,48,186,191,213,217,280,295,303,307,337,354,358,394,410,414,459,489,585,589,630,646,736,740,755,878,1016,1029,1033,1107,1122,1126,1190,1194,1212,1216,1225,1234,1245,1254,1266,1270,1297],[10,11,5],"h1",{"id":12},"qgis-core-gui-and-analysis-modules-explained",[14,15,16],"p",{},"Every PyQGIS script begins with an import, and the module you import from tells you more than where a class lives: it tells you whether the code can run without a screen, whether it belongs in a plugin or in a batch job, and whether it will still work when somebody runs it on a server. Getting this wrong is how a working script becomes an unattended job that fails at three in the morning with an error about a display.",[14,18,19,20,25,26,30,31,30,34,30,37,30,40,43,44,47],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002F","QGIS API Architecture",". It maps the modules — ",[27,28,29],"code",{},"qgis.core",", ",[27,32,33],{},"qgis.gui",[27,35,36],{},"qgis.analysis",[27,38,39],{},"qgis.utils",[27,41,42],{},"qgis.PyQt"," and ",[27,45,46],{},"processing"," — and shows how to structure code so the part that does the work never depends on the part that draws.",[14,49,50],{},[51,52,57,61,65,72,89,98,108,113,119,127,132,136,139,142,145,153,157,161,164,168,170,173,176,180,182],"svg",{"viewBox":53,"role":54,"ariaLabel":55,"xmlns":56},"0 0 760 288","img","Diagram of the PyQGIS module layers showing core at the base, analysis and processing built on it, GUI above requiring a display, and utils providing the running application's interface object","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[58,59,60],"title",{},"The module layers, and where the display requirement starts",[62,63,64],"desc",{},"The core module sits at the base and needs no display. Analysis and the Processing framework build on core and also run headless. The GUI module sits above and requires a graphical environment, and the utils module provides access to the running QGIS application and its interface object, which exists only inside QGIS desktop. A dividing line marks which layers a headless script may use.",[66,67],"rect",{"x":68,"y":68,"width":69,"height":70,"fill":71},"0","760","288","#f6f3ea",[73,74,75],"defs",{},[76,77,84],"marker",{"id":78,"viewBox":79,"refX":80,"refY":81,"markerWidth":82,"markerHeight":82,"orient":83},"modArrow","0 0 10 10","8","5","7","auto-start-reverse",[85,86],"path",{"d":87,"fill":88},"M0 0 L10 5 L0 10 z","#59645f",[90,91,97],"text",{"x":92,"y":93,"style":94,"fill":95,"textAnchor":96},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The line between these two halves is where headless scripts break",[66,99],{"x":100,"y":101,"width":102,"height":103,"rx":104,"fill":105,"stroke":106,"style":107},"40","176","680","70","10","#eef7f4","#0f766e","stroke-width:2.5",[90,109,112],{"x":92,"y":110,"style":111,"fill":106,"textAnchor":96},"202","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","qgis.core — layers, geometry, CRS, projects, rendering",[90,114,118],{"x":92,"y":115,"style":116,"fill":117,"textAnchor":96},"226","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","no display needed, safe everywhere, most of the API",[66,120],{"x":100,"y":121,"width":122,"height":123,"rx":104,"fill":124,"stroke":125,"style":126},"112","330","52","#eff3ff","#2563eb","stroke-width:2",[90,128,36],{"x":129,"y":130,"style":131,"fill":125,"textAnchor":96},"205","134","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif",[90,133,135],{"x":129,"y":134,"style":116,"fill":117,"textAnchor":96},"154","networks, interpolation, raster maths",[66,137],{"x":138,"y":121,"width":122,"height":123,"rx":104,"fill":124,"stroke":125,"style":126},"390",[90,140,46],{"x":141,"y":130,"style":131,"fill":125,"textAnchor":96},"555",[90,143,144],{"x":141,"y":134,"style":116,"fill":117,"textAnchor":96},"algorithms, models, providers",[146,147],"line",{"x1":148,"y1":149,"x2":150,"y2":149,"stroke":151,"style":152},"20","98","740","#b45309","stroke-width:2;stroke-dasharray:6 4",[90,154,156],{"x":92,"y":155,"style":131,"fill":151,"textAnchor":96},"92","above this line: a display or a running QGIS is required",[66,158],{"x":100,"y":159,"width":122,"height":159,"rx":104,"fill":160,"stroke":151,"style":107},"44","#fdf2e2",[90,162,33],{"x":129,"y":163,"style":131,"fill":151,"textAnchor":96},"62",[90,165,167],{"x":129,"y":166,"style":116,"fill":117,"textAnchor":96},"80","canvas, map tools, widgets",[66,169],{"x":138,"y":159,"width":122,"height":159,"rx":104,"fill":160,"stroke":151,"style":107},[90,171,172],{"x":141,"y":163,"style":131,"fill":151,"textAnchor":96},"qgis.utils — iface, plugins",[90,174,175],{"x":141,"y":166,"style":116,"fill":117,"textAnchor":96},"only inside QGIS desktop",[146,177],{"x1":129,"y1":101,"x2":129,"y2":178,"stroke":88,"style":179},"168","stroke-width:2;marker-end:url(#modArrow)",[146,181],{"x1":141,"y1":101,"x2":141,"y2":178,"stroke":88,"style":179},[90,183,185],{"x":92,"y":184,"style":116,"fill":88,"textAnchor":96},"272","Keep the work in the lower half and the same code runs in the console, a plugin and a cron job",[187,188,190],"h2",{"id":189},"prerequisites","Prerequisites",[192,193,194,202,210],"ul",{},[195,196,197,201],"li",{},[198,199,200],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[195,203,204,205,209],{},"The Python console, or an editor set up as in ",[21,206,208],{"href":207},"\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002F","Setting Up PyCharm for QGIS",".",[195,211,212],{},"No other setup — this page is about where things live.",[187,214,216],{"id":215},"qgiscore-almost-everything","qgis.core: almost everything",[218,219,224],"pre",{"className":220,"code":221,"language":222,"meta":223,"style":223},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsApplication, QgsProject, QgsVectorLayer, QgsRasterLayer,\n    QgsFeature, QgsGeometry, QgsPointXY,\n    QgsCoordinateReferenceSystem, QgsCoordinateTransform,\n    QgsField, QgsFeatureRequest, QgsExpression,\n    QgsVectorFileWriter, QgsSettings,\n)\n","python","",[27,225,226,244,250,256,262,268,274],{"__ignoreMap":223},[227,228,230,234,238,241],"span",{"class":146,"line":229},1,[227,231,233],{"class":232},"snl16","from",[227,235,237],{"class":236},"s95oV"," qgis.core ",[227,239,240],{"class":232},"import",[227,242,243],{"class":236}," (\n",[227,245,247],{"class":146,"line":246},2,[227,248,249],{"class":236},"    QgsApplication, QgsProject, QgsVectorLayer, QgsRasterLayer,\n",[227,251,253],{"class":146,"line":252},3,[227,254,255],{"class":236},"    QgsFeature, QgsGeometry, QgsPointXY,\n",[227,257,259],{"class":146,"line":258},4,[227,260,261],{"class":236},"    QgsCoordinateReferenceSystem, QgsCoordinateTransform,\n",[227,263,265],{"class":146,"line":264},5,[227,266,267],{"class":236},"    QgsField, QgsFeatureRequest, QgsExpression,\n",[227,269,271],{"class":146,"line":270},6,[227,272,273],{"class":236},"    QgsVectorFileWriter, QgsSettings,\n",[227,275,277],{"class":146,"line":276},7,[227,278,279],{"class":236},")\n",[14,281,282,285,286,288,289,291,292,294],{},[198,283,284],{},"Breakdown:"," If a class represents data, a transformation, a setting or a file operation, it is in ",[27,287,29],{}," — layers, features, geometry, coordinate systems, projects, expressions, symbols, renderers, layouts and the whole rendering engine. None of it requires a display, which is why a well-structured script can do all of its real work with ",[27,290,29],{}," imports only. When you are unsure where a class lives, ",[27,293,29],{}," is the correct first guess and is right most of the time.",[14,296,297,298,302],{},"The one thing that surprises people is that ",[299,300,301],"em",{},"rendering"," lives here too: producing an image from layers, exporting a layout to PDF, drawing a map to a file. Rendering is not the same as displaying, and QGIS keeps the distinction cleanly, which is precisely what makes headless map production possible.",[187,304,306],{"id":305},"qgisgui-only-where-there-is-a-screen","qgis.gui: only where there is a screen",[218,308,310],{"className":220,"code":309,"language":222,"meta":223,"style":223},"from qgis.gui import (\n    QgsMapCanvas, QgsMapTool, QgsRubberBand,\n    QgsMapLayerComboBox, QgsFileWidget, QgsOptionsPageWidget,\n)\n",[27,311,312,323,328,333],{"__ignoreMap":223},[227,313,314,316,319,321],{"class":146,"line":229},[227,315,233],{"class":232},[227,317,318],{"class":236}," qgis.gui ",[227,320,240],{"class":232},[227,322,243],{"class":236},[227,324,325],{"class":146,"line":246},[227,326,327],{"class":236},"    QgsMapCanvas, QgsMapTool, QgsRubberBand,\n",[227,329,330],{"class":146,"line":252},[227,331,332],{"class":236},"    QgsMapLayerComboBox, QgsFileWidget, QgsOptionsPageWidget,\n",[227,334,335],{"class":146,"line":258},[227,336,279],{"class":236},[14,338,339,341,342,344,345,349,350,209],{},[198,340,284],{}," Anything the user interacts with — the canvas, map tools, rubber bands, the layer combo boxes and file widgets you put in plugin dialogs — lives in ",[27,343,33],{},". Importing this module in a script running without a display can fail outright or, worse, succeed and then crash the first time a widget is constructed. In a plugin it is entirely appropriate; in a batch job it is a design error, and the fastest way to make a script that \"works on my machine\" and nowhere else. The widgets themselves are covered in ",[21,346,348],{"href":347},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for GIS Interfaces"," and the map tools in ",[21,351,353],{"href":352},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","Create a Custom Map Tool in PyQGIS",[187,355,357],{"id":356},"qgisanalysis-and-processing","qgis.analysis and processing",[218,359,361],{"className":220,"code":360,"language":222,"meta":223,"style":223},"from qgis.analysis import QgsNativeAlgorithms, QgsZonalStatistics, QgsGraphBuilder\nimport processing\nfrom processing.core.Processing import Processing\n",[27,362,363,375,382],{"__ignoreMap":223},[227,364,365,367,370,372],{"class":146,"line":229},[227,366,233],{"class":232},[227,368,369],{"class":236}," qgis.analysis ",[227,371,240],{"class":232},[227,373,374],{"class":236}," QgsNativeAlgorithms, QgsZonalStatistics, QgsGraphBuilder\n",[227,376,377,379],{"class":146,"line":246},[227,378,240],{"class":232},[227,380,381],{"class":236}," processing\n",[227,383,384,386,389,391],{"class":146,"line":252},[227,385,233],{"class":232},[227,387,388],{"class":236}," processing.core.Processing ",[227,390,240],{"class":232},[227,392,393],{"class":236}," Processing\n",[14,395,396,398,399,401,402,404,405,409],{},[198,397,284],{}," ",[27,400,36],{}," holds the analytical machinery that is not simply data handling: network analysis and shortest paths, interpolation, zonal statistics, geometry checking. Much of it is also exposed as Processing algorithms, and calling the algorithm is usually the better choice — it handles progress, cancellation and output creation for you. ",[27,403,46],{}," is the framework's Python interface, and in a standalone script it needs initialising explicitly along with the native algorithm provider, which is one of the steps described in ",[21,406,408],{"href":407},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","Running Python Scripts Outside QGIS Desktop",". Both run happily without a display.",[187,411,413],{"id":412},"qgisutils-the-running-application","qgis.utils: the running application",[218,415,417],{"className":220,"code":416,"language":222,"meta":223,"style":223},"from qgis.utils import iface, plugins, reloadPlugin\n\nprint(iface.activeLayer())\nprint(list(plugins.keys()))\n",[27,418,419,431,437,446],{"__ignoreMap":223},[227,420,421,423,426,428],{"class":146,"line":229},[227,422,233],{"class":232},[227,424,425],{"class":236}," qgis.utils ",[227,427,240],{"class":232},[227,429,430],{"class":236}," iface, plugins, reloadPlugin\n",[227,432,433],{"class":146,"line":246},[227,434,436],{"emptyLinePlaceholder":435},true,"\n",[227,438,439,443],{"class":146,"line":252},[227,440,442],{"class":441},"sDLfK","print",[227,444,445],{"class":236},"(iface.activeLayer())\n",[227,447,448,450,453,456],{"class":146,"line":258},[227,449,442],{"class":441},[227,451,452],{"class":236},"(",[227,454,455],{"class":441},"list",[227,457,458],{"class":236},"(plugins.keys()))\n",[14,460,461,398,463,465,466,469,470,473,474,477,478,480,481,484,485,488],{},[198,462,284],{},[27,464,39],{}," is the bridge to a ",[299,467,468],{},"running QGIS desktop",": ",[27,471,472],{},"iface"," is the interface object that gives access to the canvas, the menus, the message bar and the active layer, and ",[27,475,476],{},"plugins"," is the dictionary of loaded plugins by folder name. None of it exists outside QGIS desktop — in a standalone script ",[27,479,472],{}," is ",[27,482,483],{},"None",", and code that assumes otherwise fails immediately. This is the single most common reason a console script cannot be scheduled: it reaches for ",[27,486,487],{},"iface.activeLayer()"," instead of taking the layer as a parameter.",[14,490,491],{},[51,492,495,498,501,504,511,514,520,525,533,538,541,545,548,552,556,561,565,568,572,574,577,579,582],{"viewBox":493,"role":54,"ariaLabel":494,"xmlns":56},"0 0 760 252","Comparison of a script that mixes interface access into its logic against one that isolates the interface at the edges, showing which of the two can be scheduled",[58,496,497],{},"The shape that runs in both worlds",[62,499,500],{},"On the left, a function reaches for the active layer and pushes messages to the message bar, so it only works inside QGIS desktop. On the right, the same work is a plain function taking a layer and returning a result, wrapped by a thin plugin layer that supplies the layer and displays the result. Only the second version can also be called by a scheduled job.",[66,502],{"x":68,"y":68,"width":69,"height":503,"fill":71},"252",[73,505,506],{},[76,507,509],{"id":508,"viewBox":79,"refX":80,"refY":81,"markerWidth":82,"markerHeight":82,"orient":83},"modShapeArrow",[85,510],{"d":87,"fill":117},[90,512,513],{"x":92,"y":93,"style":94,"fill":95,"textAnchor":96},"Push the interface to the edges and the middle becomes reusable",[66,515],{"x":148,"y":516,"width":517,"height":518,"rx":104,"fill":160,"stroke":519,"style":107},"48","348","180","#b91c1c",[90,521,524],{"x":522,"y":523,"style":111,"fill":519,"textAnchor":96},"194","74","interface woven through",[66,526],{"x":159,"y":527,"width":528,"height":529,"rx":530,"fill":531,"stroke":88,"style":532},"90","300","34","6","#fffdf7","stroke-width:1.5",[90,534,537],{"x":535,"y":121,"style":536,"fill":117},"60","font-size:11px;font-family:sans-serif","layer = iface.activeLayer()",[66,539],{"x":159,"y":540,"width":528,"height":529,"rx":530,"fill":531,"stroke":88,"style":532},"130",[90,542,544],{"x":535,"y":543,"style":536,"fill":117},"152","do the analysis",[66,546],{"x":159,"y":547,"width":528,"height":529,"rx":530,"fill":531,"stroke":88,"style":532},"170",[90,549,551],{"x":535,"y":550,"style":536,"fill":117},"192","iface.messageBar().push(...)",[90,553,555],{"x":522,"y":554,"style":131,"fill":519,"textAnchor":96},"220","cannot be scheduled",[66,557],{"x":558,"y":516,"width":517,"height":518,"rx":104,"fill":559,"stroke":560,"style":107},"392","#edf8e9","#15803d",[90,562,564],{"x":563,"y":523,"style":111,"fill":560,"textAnchor":96},"566","interface at the edges",[66,566],{"x":567,"y":527,"width":528,"height":529,"rx":530,"fill":531,"stroke":151,"style":532},"416",[90,569,571],{"x":570,"y":121,"style":536,"fill":117},"432","plugin: get the layer from iface",[66,573],{"x":567,"y":540,"width":528,"height":529,"rx":530,"fill":105,"stroke":106,"style":126},[90,575,576],{"x":570,"y":543,"style":536,"fill":117},"analyse(layer) — core only",[66,578],{"x":567,"y":547,"width":528,"height":529,"rx":530,"fill":531,"stroke":151,"style":532},[90,580,581],{"x":570,"y":550,"style":536,"fill":117},"plugin: display the result",[90,583,584],{"x":563,"y":554,"style":131,"fill":560,"textAnchor":96},"plugin, console and cron",[187,586,588],{"id":587},"qgispyqt-import-qt-through-qgis","qgis.PyQt: import Qt through QGIS",[218,590,592],{"className":220,"code":591,"language":222,"meta":223,"style":223},"from qgis.PyQt.QtCore import QCoreApplication, QVariant, Qt\nfrom qgis.PyQt.QtWidgets import QAction, QMessageBox\nfrom qgis.PyQt.QtGui import QColor, QIcon\n",[27,593,594,606,618],{"__ignoreMap":223},[227,595,596,598,601,603],{"class":146,"line":229},[227,597,233],{"class":232},[227,599,600],{"class":236}," qgis.PyQt.QtCore ",[227,602,240],{"class":232},[227,604,605],{"class":236}," QCoreApplication, QVariant, Qt\n",[227,607,608,610,613,615],{"class":146,"line":246},[227,609,233],{"class":232},[227,611,612],{"class":236}," qgis.PyQt.QtWidgets ",[227,614,240],{"class":232},[227,616,617],{"class":236}," QAction, QMessageBox\n",[227,619,620,622,625,627],{"class":146,"line":252},[227,621,233],{"class":232},[227,623,624],{"class":236}," qgis.PyQt.QtGui ",[227,626,240],{"class":232},[227,628,629],{"class":236}," QColor, QIcon\n",[14,631,632,634,635,637,638,641,642,645],{},[198,633,284],{}," Always import Qt classes through ",[27,636,42],{}," rather than directly from ",[27,639,640],{},"PyQt5",". The shim resolves to whichever Qt binding the running QGIS was built against, which is what lets the same plugin work on a Qt 5 build and a Qt 6 build without a change. Direct ",[27,643,644],{},"from PyQt5.QtWidgets import ..."," imports work today and break on the next major QGIS release; they are also the reason a plugin fails to load on someone else's platform while working perfectly on yours.",[14,647,648],{},[51,649,652,655,658,661,668,671,674,679,683,686,690,693,696,701,705,708,712,715,721,724,728,732],{"viewBox":650,"role":54,"ariaLabel":651,"xmlns":56},"0 0 760 246","Diagram showing the qgis PyQt shim resolving to whichever Qt binding the running QGIS was built against, while a direct PyQt5 import fails on a Qt 6 build",[58,653,654],{},"Why the import path matters more than the class name",[62,656,657],{},"An import through qgis.PyQt resolves at run time to the Qt binding the running QGIS was built against, so the same code works on a Qt 5 build and a Qt 6 build. A direct import from PyQt5 works only where that binding is present, and fails to load the plugin entirely on any other build.",[66,659],{"x":68,"y":68,"width":69,"height":660,"fill":71},"246",[73,662,663],{},[76,664,666],{"id":665,"viewBox":79,"refX":80,"refY":81,"markerWidth":82,"markerHeight":82,"orient":83},"qtArrow",[85,667],{"d":87,"fill":106},[90,669,670],{"x":92,"y":93,"style":94,"fill":95,"textAnchor":96},"One import line decides whether the plugin is portable",[66,672],{"x":148,"y":535,"width":673,"height":535,"rx":80,"fill":105,"stroke":106,"style":107},"240",[90,675,678],{"x":676,"y":677,"style":131,"fill":106,"textAnchor":96},"140","84","from qgis.PyQt import ...",[90,680,682],{"x":676,"y":681,"style":116,"fill":117,"textAnchor":96},"104","resolved at run time",[66,684],{"x":148,"y":685,"width":673,"height":535,"rx":80,"fill":160,"stroke":519,"style":107},"150",[90,687,689],{"x":676,"y":688,"style":131,"fill":519,"textAnchor":96},"174","from PyQt5 import ...",[90,691,692],{"x":676,"y":522,"style":116,"fill":117,"textAnchor":96},"pinned at authoring time",[66,694],{"x":695,"y":123,"width":528,"height":535,"rx":80,"fill":531,"stroke":88,"style":126},"440",[90,697,700],{"x":698,"y":699,"style":131,"fill":95,"textAnchor":96},"590","76","a Qt 5 build of QGIS",[90,702,704],{"x":698,"y":703,"style":116,"fill":560,"textAnchor":96},"96","both imports work",[66,706],{"x":695,"y":707,"width":528,"height":535,"rx":80,"fill":531,"stroke":88,"style":126},"132",[90,709,711],{"x":698,"y":710,"style":131,"fill":95,"textAnchor":96},"156","a Qt 6 build of QGIS",[90,713,714],{"x":698,"y":101,"style":116,"fill":519,"textAnchor":96},"only the shim works",[146,716],{"x1":717,"y1":718,"x2":719,"y2":699,"stroke":106,"style":720},"260","82","434","stroke-width:2;marker-end:url(#qtArrow)",[146,722],{"x1":717,"y1":723,"x2":719,"y2":710,"stroke":106,"style":720},"100",[146,725],{"x1":717,"y1":726,"x2":719,"y2":703,"stroke":88,"style":727},"172","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#qtArrow)",[146,729],{"x1":717,"y1":730,"x2":719,"y2":101,"stroke":519,"style":731},"190","stroke-width:2;stroke-dasharray:5 4",[90,733,735],{"x":517,"y":734,"style":116,"fill":88,"textAnchor":96},"224","the dashed path stops here — the plugin never loads",[187,737,739],{"id":738},"structure-code-so-the-work-is-portable","Structure code so the work is portable",[14,741,742,743,30,745,43,747,749,750,43,752,754],{},"The practical payoff of understanding the modules is a shape: keep the work in plain functions that import only ",[27,744,29],{},[27,746,36],{},[27,748,46],{},", and confine ",[27,751,33],{},[27,753,39],{}," to a thin outer layer.",[218,756,758],{"className":220,"code":757,"language":222,"meta":223,"style":223},"# analysis.py — no GUI, no iface, testable and schedulable\nfrom qgis.core import QgsVectorLayer, QgsFeatureRequest\n\n\ndef summarise_areas(layer, minimum_area=0.0):\n    request = QgsFeatureRequest().setSubsetOfAttributes([\"ref\"], layer.fields())\n    return {\n        feature[\"ref\"]: feature.geometry().area()\n        for feature in layer.getFeatures(request)\n        if feature.geometry().area() >= minimum_area\n    }\n",[27,759,760,766,777,781,785,806,823,831,842,857,872],{"__ignoreMap":223},[227,761,762],{"class":146,"line":229},[227,763,765],{"class":764},"sjoCn","# analysis.py — no GUI, no iface, testable and schedulable\n",[227,767,768,770,772,774],{"class":146,"line":246},[227,769,233],{"class":232},[227,771,237],{"class":236},[227,773,240],{"class":232},[227,775,776],{"class":236}," QgsVectorLayer, QgsFeatureRequest\n",[227,778,779],{"class":146,"line":252},[227,780,436],{"emptyLinePlaceholder":435},[227,782,783],{"class":146,"line":258},[227,784,436],{"emptyLinePlaceholder":435},[227,786,787,790,794,797,800,803],{"class":146,"line":264},[227,788,789],{"class":232},"def",[227,791,793],{"class":792},"svObZ"," summarise_areas",[227,795,796],{"class":236},"(layer, minimum_area",[227,798,799],{"class":232},"=",[227,801,802],{"class":441},"0.0",[227,804,805],{"class":236},"):\n",[227,807,808,811,813,816,820],{"class":146,"line":270},[227,809,810],{"class":236},"    request ",[227,812,799],{"class":232},[227,814,815],{"class":236}," QgsFeatureRequest().setSubsetOfAttributes([",[227,817,819],{"class":818},"sU2Wk","\"ref\"",[227,821,822],{"class":236},"], layer.fields())\n",[227,824,825,828],{"class":146,"line":276},[227,826,827],{"class":232},"    return",[227,829,830],{"class":236}," {\n",[227,832,834,837,839],{"class":146,"line":833},8,[227,835,836],{"class":236},"        feature[",[227,838,819],{"class":818},[227,840,841],{"class":236},"]: feature.geometry().area()\n",[227,843,845,848,851,854],{"class":146,"line":844},9,[227,846,847],{"class":232},"        for",[227,849,850],{"class":236}," feature ",[227,852,853],{"class":232},"in",[227,855,856],{"class":236}," layer.getFeatures(request)\n",[227,858,860,863,866,869],{"class":146,"line":859},10,[227,861,862],{"class":232},"        if",[227,864,865],{"class":236}," feature.geometry().area() ",[227,867,868],{"class":232},">=",[227,870,871],{"class":236}," minimum_area\n",[227,873,875],{"class":146,"line":874},11,[227,876,877],{"class":236},"    }\n",[218,879,881],{"className":220,"code":880,"language":222,"meta":223,"style":223},"# plugin.py — the only file that knows about the interface\nfrom qgis.utils import iface\nfrom .analysis import summarise_areas\n\n\ndef run():\n    layer = iface.activeLayer()\n    if layer is None:\n        iface.messageBar().pushWarning(\"Parcel Tools\", \"Select a layer first\")\n        return\n    results = summarise_areas(layer)\n    iface.messageBar().pushInfo(\"Parcel Tools\", f\"{len(results)} parcels summarised\")\n",[27,882,883,888,899,911,915,919,929,939,956,971,976,986],{"__ignoreMap":223},[227,884,885],{"class":146,"line":229},[227,886,887],{"class":764},"# plugin.py — the only file that knows about the interface\n",[227,889,890,892,894,896],{"class":146,"line":246},[227,891,233],{"class":232},[227,893,425],{"class":236},[227,895,240],{"class":232},[227,897,898],{"class":236}," iface\n",[227,900,901,903,906,908],{"class":146,"line":252},[227,902,233],{"class":232},[227,904,905],{"class":236}," .analysis ",[227,907,240],{"class":232},[227,909,910],{"class":236}," summarise_areas\n",[227,912,913],{"class":146,"line":258},[227,914,436],{"emptyLinePlaceholder":435},[227,916,917],{"class":146,"line":264},[227,918,436],{"emptyLinePlaceholder":435},[227,920,921,923,926],{"class":146,"line":270},[227,922,789],{"class":232},[227,924,925],{"class":792}," run",[227,927,928],{"class":236},"():\n",[227,930,931,934,936],{"class":146,"line":276},[227,932,933],{"class":236},"    layer ",[227,935,799],{"class":232},[227,937,938],{"class":236}," iface.activeLayer()\n",[227,940,941,944,947,950,953],{"class":146,"line":833},[227,942,943],{"class":232},"    if",[227,945,946],{"class":236}," layer ",[227,948,949],{"class":232},"is",[227,951,952],{"class":441}," None",[227,954,955],{"class":236},":\n",[227,957,958,961,964,966,969],{"class":146,"line":844},[227,959,960],{"class":236},"        iface.messageBar().pushWarning(",[227,962,963],{"class":818},"\"Parcel Tools\"",[227,965,30],{"class":236},[227,967,968],{"class":818},"\"Select a layer first\"",[227,970,279],{"class":236},[227,972,973],{"class":146,"line":859},[227,974,975],{"class":232},"        return\n",[227,977,978,981,983],{"class":146,"line":874},[227,979,980],{"class":236},"    results ",[227,982,799],{"class":232},[227,984,985],{"class":236}," summarise_areas(layer)\n",[227,987,989,992,994,996,999,1002,1005,1008,1011,1014],{"class":146,"line":988},12,[227,990,991],{"class":236},"    iface.messageBar().pushInfo(",[227,993,963],{"class":818},[227,995,30],{"class":236},[227,997,998],{"class":232},"f",[227,1000,1001],{"class":818},"\"",[227,1003,1004],{"class":441},"{len",[227,1006,1007],{"class":236},"(results)",[227,1009,1010],{"class":441},"}",[227,1012,1013],{"class":818}," parcels summarised\"",[227,1015,279],{"class":236},[14,1017,1018,1020,1021,1023,1024,1028],{},[198,1019,284],{}," The analysis function takes a layer and returns data, which makes it callable from a plugin, from the console, from a scheduled script and from a unit test without any of them needing a display. The plugin file is the only place that imports ",[27,1022,39],{},", and it does nothing but fetch input and report output. This separation costs one extra file and pays for itself the first time somebody asks whether the tool can run nightly — the answer is yes, with no rewrite. It is also what makes the test setup in ",[21,1025,1027],{"href":1026},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","Unit Test a QGIS Plugin with pytest"," straightforward rather than an exercise in mocking the entire application.",[187,1030,1032],{"id":1031},"qgis-version-compatibility","QGIS version compatibility",[1034,1035,1036,1052],"table",{},[1037,1038,1039],"thead",{},[1040,1041,1042,1046,1049],"tr",{},[1043,1044,1045],"th",{},"QGIS version",[1043,1047,1048],{},"Python",[1043,1050,1051],{},"Notes",[1053,1054,1055,1070,1080,1091],"tbody",{},[1040,1056,1057,1061,1064],{},[1058,1059,1060],"td",{},"3.22 LTR",[1058,1062,1063],{},"3.9",[1058,1065,1066,1067,1069],{},"Module layout as described; ",[27,1068,42],{}," resolves to PyQt5.",[1040,1071,1072,1075,1077],{},[1058,1073,1074],{},"3.28 LTR",[1058,1076,1063],{},[1058,1078,1079],{},"Identical.",[1040,1081,1082,1085,1088],{},[1058,1083,1084],{},"3.34 LTR",[1058,1086,1087],{},"3.12",[1058,1089,1090],{},"Baseline for this page.",[1040,1092,1093,1096,1098],{},[1058,1094,1095],{},"3.40 \u002F 3.44",[1058,1097,1087],{},[1058,1099,1100,1101,1103,1104,1106],{},"Same layout; builds against Qt 6 exist, which is exactly why ",[27,1102,42],{}," rather than ",[27,1105,640],{}," matters.",[14,1108,1109,1110,1113,1114,1117,1118,209],{},"Classes do occasionally move between modules across major versions — ",[27,1111,1112],{},"QgsMapLayerComboBox"," and friends have always been GUI, but some analysis classes migrated into core. ",[27,1115,1116],{},"dir()"," and the API documentation settle it in seconds, using the introspection covered in ",[21,1119,1121],{"href":1120},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fexplore-pyqgis-api-with-dir-and-help\u002F","Explore the PyQGIS API with dir() and help()",[187,1123,1125],{"id":1124},"troubleshooting","Troubleshooting",[192,1127,1128,1140,1149,1160,1169,1181],{},[195,1129,1130,1139],{},[198,1131,1132,1135,1136,1138],{},[27,1133,1134],{},"ImportError"," on ",[27,1137,33],{}," in a headless script."," Correct behaviour — the script should not need it. Move the GUI code into the plugin layer.",[195,1141,1142,1148],{},[198,1143,1144,480,1146,209],{},[27,1145,472],{},[27,1147,483],{}," You are not inside QGIS desktop. Pass the layer in as an argument instead of reaching for the active one.",[195,1150,1151,1154,1155,1157,1158,209],{},[198,1152,1153],{},"A plugin fails to load on another platform."," A direct ",[27,1156,640],{}," import. Change it to ",[27,1159,42],{},[195,1161,1162,1168],{},[198,1163,1164,1167],{},[27,1165,1166],{},"processing.run()"," reports no algorithms."," In a standalone script, Processing and the native provider must be initialised explicitly before use.",[195,1170,1171,1174,1175,43,1178,209],{},[198,1172,1173],{},"A class cannot be imported from where a tutorial says."," Either QGIS 2 code, or the class has moved. Check ",[27,1176,1177],{},"dir(qgis.core)",[27,1179,1180],{},"dir(qgis.analysis)",[195,1182,1183,1186,1187,1189],{},[198,1184,1185],{},"Everything works interactively and nothing works on the server."," Almost always a GUI or ",[27,1188,472],{}," dependency buried in a function that looks like pure logic.",[187,1191,1193],{"id":1192},"conclusion","Conclusion",[14,1195,1196,1198,1199,43,1201,1203,1204,43,1206,1208,1209,1211],{},[27,1197,29],{}," holds the data model and the rendering engine and needs no display; ",[27,1200,36],{},[27,1202,46],{}," build on it and are equally portable; ",[27,1205,33],{},[27,1207,39],{}," require a running QGIS desktop. Import Qt through ",[27,1210,42],{}," so the code survives a binding change, and structure scripts so the work lives in functions that take arguments and return values — the same code then runs in the console, in a plugin and in a scheduled job.",[187,1213,1215],{"id":1214},"frequently-asked-questions","Frequently Asked Questions",[14,1217,1218,1224],{},[198,1219,1220,1221,1223],{},"Can I use ",[27,1222,33],{}," classes in a Processing algorithm?","\nNo. Algorithms must run headless and on background threads. Anything the user needs to choose becomes an algorithm parameter instead.",[14,1226,1227,1233],{},[198,1228,1229,1230,1232],{},"Why import Qt through ",[27,1231,42],{},"?","\nSo the code resolves to whichever Qt version the running QGIS uses. It is a one-word change that makes a plugin portable across builds.",[14,1235,1236,1244],{},[198,1237,1238,1239,1241,1242,1232],{},"Is ",[27,1240,46],{}," part of ",[27,1243,29],{},"\nNo — it is a separate Python package shipped with QGIS. In a standalone script it needs explicit initialisation, unlike in the console where QGIS has already done it.",[14,1246,1247,1250,1251,1253],{},[198,1248,1249],{},"Where do symbols and renderers live?","\nIn ",[27,1252,29],{},". Styling is data about how to draw, not interface, so it is usable headless — which is what makes automated cartography possible.",[14,1255,1256,1259,1260,1262,1263,1265],{},[198,1257,1258],{},"How do I know which module a class is in?","\nTry ",[27,1261,29],{}," first, then check the API documentation, or filter ",[27,1264,1116],{}," on each module. The class page also states the module explicitly.",[187,1267,1269],{"id":1268},"related","Related",[192,1271,1272,1277,1281,1285,1291],{},[195,1273,1274,1276],{},[21,1275,24],{"href":23}," — the guide this recipe belongs to",[195,1278,1279],{},[21,1280,1121],{"href":1120},[195,1282,1283],{},[21,1284,408],{"href":407},[195,1286,1287],{},[21,1288,1290],{"href":1289},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","Headless QGIS and Server Automation",[195,1292,1293],{},[21,1294,1296],{"href":1295},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate and Structure",[1298,1299,1300],"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 .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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":223,"searchDepth":246,"depth":246,"links":1302},[1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314],{"id":189,"depth":246,"text":190},{"id":215,"depth":246,"text":216},{"id":305,"depth":246,"text":306},{"id":356,"depth":246,"text":357},{"id":412,"depth":246,"text":413},{"id":587,"depth":246,"text":588},{"id":738,"depth":246,"text":739},{"id":1031,"depth":246,"text":1032},{"id":1124,"depth":246,"text":1125},{"id":1192,"depth":246,"text":1193},{"id":1214,"depth":246,"text":1215},{"id":1268,"depth":246,"text":1269},"Know which import belongs where — what lives in qgis.core, qgis.gui, qgis.analysis, qgis.utils and processing, why a headless script must avoid the GUI module, and how to structure code that runs both ways.","md",{"slug":1318,"type":1319,"breadcrumb":1320,"datePublished":1321,"dateModified":1321},"qgis-core-gui-analysis-modules-explained","article","Core, GUI & Analysis","2026-08-15","\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-core-gui-analysis-modules-explained",{"title":5,"description":1315},"pyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-core-gui-analysis-modules-explained\u002Findex","u4_D5nVQ-IynXHz8sQ44ixOUQKQBFK8aw-8i-dDcP7k",1786789584627]