[{"data":1,"prerenderedAt":1346},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Fuse-pyqgis-in-jupyter-notebook":3},{"id":4,"title":5,"body":6,"description":1335,"extension":1336,"meta":1337,"navigation":237,"path":1342,"seo":1343,"stem":1344,"__hash__":1345},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Fuse-pyqgis-in-jupyter-notebook\u002Findex.md","Use PyQGIS in a Jupyter Notebook",{"type":7,"value":8,"toc":1322},"minimark",[9,13,17,26,163,168,190,194,202,372,394,398,502,523,527,530,733,749,850,854,857,973,982,1066,1070,1080,1083,1087,1152,1155,1159,1230,1234,1240,1244,1250,1259,1265,1275,1281,1287,1291,1318],[10,11,5],"h1",{"id":12},"use-pyqgis-in-a-jupyter-notebook",[14,15,16],"p",{},"A notebook is a good fit for the kind of GIS work that is really analysis: try a buffer distance, look at the result, adjust, keep a record of what you did. PyQGIS runs there perfectly well — it is a normal Python library once the paths are right — and the result is a document combining code, tables, maps and the reasoning behind them, which is far more useful to a colleague than a script and a folder of outputs.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002F","Virtual Environments for GIS",". It covers making the QGIS libraries importable from a notebook kernel, initialising the application headless, rendering a map into a cell, and combining PyQGIS with the wider Python data stack without either environment interfering with the other.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,100,107,111,114,117,122,126,129,132,139,143,150,155,159],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 270","img","Diagram of a notebook kernel with the QGIS libraries added to its path, initialising an offscreen application and rendering map images inline alongside data frames","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"What a notebook needs before PyQGIS will import",[40,41,42],"desc",{},"The notebook kernel starts as an ordinary Python environment. Three additions make PyQGIS work: the QGIS Python library folder on the path, the QGIS prefix path set so resources are found, and an application instance initialised with the offscreen platform. After that the kernel can build layers, run algorithms and render images inline next to data frames and charts.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","270","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"jupArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Three additions turn a kernel into a QGIS environment",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"20","52","200","76","#eff3ff","#2563eb","stroke-width:2",[68,86,90],{"x":87,"y":88,"style":89,"fill":83,"textAnchor":74},"120","80","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","the kernel",[68,92,95],{"x":87,"y":93,"style":94,"fill":66,"textAnchor":74},"102","text-anchor:middle;font-size:11px;font-family:sans-serif","pandas, matplotlib",[68,97,99],{"x":87,"y":87,"style":94,"fill":98,"textAnchor":74},"#59645f","your usual environment",[44,101],{"x":102,"y":79,"width":103,"height":81,"rx":58,"fill":104,"stroke":105,"style":106},"252","228","#eef7f4","#0f766e","stroke-width:2.5",[68,108,110],{"x":109,"y":88,"style":89,"fill":105,"textAnchor":74},"366","plus the QGIS libraries",[68,112,113],{"x":109,"y":93,"style":94,"fill":66,"textAnchor":74},"sys.path and prefix path",[68,115,116],{"x":109,"y":87,"style":94,"fill":98,"textAnchor":74},"same Python version required",[44,118],{"x":119,"y":79,"width":103,"height":81,"rx":58,"fill":120,"stroke":121,"style":106},"512","#edf8e9","#15803d",[68,123,125],{"x":124,"y":88,"style":89,"fill":121,"textAnchor":74},"626","plus an application",[68,127,128],{"x":124,"y":93,"style":94,"fill":66,"textAnchor":74},"initialised offscreen",[68,130,131],{"x":124,"y":87,"style":94,"fill":98,"textAnchor":74},"once per kernel",[133,134],"line",{"x1":135,"y1":136,"x2":137,"y2":136,"stroke":66,"style":138},"220","90","246","stroke-width:2;marker-end:url(#jupArrow)",[133,140],{"x1":141,"y1":136,"x2":142,"y2":136,"stroke":66,"style":138},"480","506",[44,144],{"x":87,"y":145,"width":146,"height":147,"rx":148,"fill":149,"stroke":98,"style":84},"160","520","82","10","#fffdf7",[68,151,154],{"x":70,"y":152,"style":153,"fill":73,"textAnchor":74},"186","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","what you get in the notebook",[68,156,158],{"x":70,"y":157,"style":94,"fill":66,"textAnchor":74},"210","layers and algorithms as normal objects, maps rendered inline as images",[68,160,162],{"x":70,"y":161,"style":94,"fill":66,"textAnchor":74},"230","attributes as data frames, all in one document with the reasoning",[164,165,167],"h2",{"id":166},"prerequisites","Prerequisites",[169,170,171,179,187],"ul",{},[172,173,174,178],"li",{},[175,176,177],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, installed on the machine.",[172,180,181,182,186],{},"A notebook environment whose Python is the ",[183,184,185],"em",{},"same minor version"," as the one QGIS uses — this is the constraint that decides whether any of this works.",[172,188,189],{},"Jupyter itself, installed either into the QGIS Python or into an environment that can reach the QGIS libraries.",[164,191,193],{"id":192},"make-the-qgis-libraries-importable","Make the QGIS libraries importable",[14,195,196,197,201],{},"The simplest arrangement is to install Jupyter into the QGIS Python, using the technique from ",[21,198,200],{"href":199},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Finstall-python-packages-into-qgis\u002F","Install Python Packages into the QGIS Environment",". Everything then works with no path manipulation at all. Where that is not desirable, add the QGIS libraries to your own environment:",[203,204,209],"pre",{"className":205,"code":206,"language":207,"meta":208,"style":208},"language-python shiki shiki-themes github-dark","import os\nimport sys\n\nQGIS_PREFIX = \"\u002Fusr\"                      # \u002FApplications\u002FQGIS.app\u002FContents\u002FMacOS on macOS\nsys.path.append(f\"{QGIS_PREFIX}\u002Fshare\u002Fqgis\u002Fpython\")\nsys.path.append(f\"{QGIS_PREFIX}\u002Fshare\u002Fqgis\u002Fpython\u002Fplugins\")   # for the processing package\n\nos.environ[\"QT_QPA_PLATFORM\"] = \"offscreen\"\n\nfrom qgis.core import QgsApplication\nprint(QgsApplication.qgisVersion() if hasattr(QgsApplication, \"qgisVersion\") else \"imported\")\n","python","",[210,211,212,224,232,239,257,278,298,303,321,326,340],"code",{"__ignoreMap":208},[213,214,216,220],"span",{"class":133,"line":215},1,[213,217,219],{"class":218},"snl16","import",[213,221,223],{"class":222},"s95oV"," os\n",[213,225,227,229],{"class":133,"line":226},2,[213,228,219],{"class":218},[213,230,231],{"class":222}," sys\n",[213,233,235],{"class":133,"line":234},3,[213,236,238],{"emptyLinePlaceholder":237},true,"\n",[213,240,242,246,249,253],{"class":133,"line":241},4,[213,243,245],{"class":244},"sDLfK","QGIS_PREFIX",[213,247,248],{"class":218}," =",[213,250,252],{"class":251},"sU2Wk"," \"\u002Fusr\"",[213,254,256],{"class":255},"sjoCn","                      # \u002FApplications\u002FQGIS.app\u002FContents\u002FMacOS on macOS\n",[213,258,260,263,266,269,272,275],{"class":133,"line":259},5,[213,261,262],{"class":222},"sys.path.append(",[213,264,265],{"class":218},"f",[213,267,268],{"class":251},"\"",[213,270,271],{"class":244},"{QGIS_PREFIX}",[213,273,274],{"class":251},"\u002Fshare\u002Fqgis\u002Fpython\"",[213,276,277],{"class":222},")\n",[213,279,281,283,285,287,289,292,295],{"class":133,"line":280},6,[213,282,262],{"class":222},[213,284,265],{"class":218},[213,286,268],{"class":251},[213,288,271],{"class":244},[213,290,291],{"class":251},"\u002Fshare\u002Fqgis\u002Fpython\u002Fplugins\"",[213,293,294],{"class":222},")   ",[213,296,297],{"class":255},"# for the processing package\n",[213,299,301],{"class":133,"line":300},7,[213,302,238],{"emptyLinePlaceholder":237},[213,304,306,309,312,315,318],{"class":133,"line":305},8,[213,307,308],{"class":222},"os.environ[",[213,310,311],{"class":251},"\"QT_QPA_PLATFORM\"",[213,313,314],{"class":222},"] ",[213,316,317],{"class":218},"=",[213,319,320],{"class":251}," \"offscreen\"\n",[213,322,324],{"class":133,"line":323},9,[213,325,238],{"emptyLinePlaceholder":237},[213,327,329,332,335,337],{"class":133,"line":328},10,[213,330,331],{"class":218},"from",[213,333,334],{"class":222}," qgis.core ",[213,336,219],{"class":218},[213,338,339],{"class":222}," QgsApplication\n",[213,341,343,346,349,352,355,358,361,364,367,370],{"class":133,"line":342},11,[213,344,345],{"class":244},"print",[213,347,348],{"class":222},"(QgsApplication.qgisVersion() ",[213,350,351],{"class":218},"if",[213,353,354],{"class":244}," hasattr",[213,356,357],{"class":222},"(QgsApplication, ",[213,359,360],{"class":251},"\"qgisVersion\"",[213,362,363],{"class":222},") ",[213,365,366],{"class":218},"else",[213,368,369],{"class":251}," \"imported\"",[213,371,277],{"class":222},[14,373,374,377,378,381,382,385,386,389,390,393],{},[175,375,376],{},"Breakdown:"," Two folders matter: the Python bindings themselves and the plugins folder, which is where the ",[210,379,380],{},"processing"," package lives — omit the second and every import works except Processing, which is a confusing half-failure. ",[210,383,384],{},"QT_QPA_PLATFORM"," set to ",[210,387,388],{},"offscreen"," must be in place ",[183,391,392],{},"before"," Qt is imported, which is why it appears above the import rather than after it; without it, a kernel on a machine with no display fails with an error about not connecting to a display server. The prefix differs by platform, and on Windows it is the OSGeo4W root with its own set of environment variables, which makes installing Jupyter into the QGIS Python distinctly the easier route there.",[164,395,397],{"id":396},"initialise-the-application-once","Initialise the application once",[203,399,401],{"className":205,"code":400,"language":207,"meta":208,"style":208},"from qgis.core import QgsApplication\n\nqgs = QgsApplication([], False)\nqgs.setPrefixPath(QGIS_PREFIX, True)\nqgs.initQgis()\n\nfrom processing.core.Processing import Processing\nimport processing\nProcessing.initialize()\n\nprint(len(QgsApplication.processingRegistry().algorithms()), \"algorithms available\")\n",[210,402,403,413,417,432,447,452,456,468,475,480,484],{"__ignoreMap":208},[213,404,405,407,409,411],{"class":133,"line":215},[213,406,331],{"class":218},[213,408,334],{"class":222},[213,410,219],{"class":218},[213,412,339],{"class":222},[213,414,415],{"class":133,"line":226},[213,416,238],{"emptyLinePlaceholder":237},[213,418,419,422,424,427,430],{"class":133,"line":234},[213,420,421],{"class":222},"qgs ",[213,423,317],{"class":218},[213,425,426],{"class":222}," QgsApplication([], ",[213,428,429],{"class":244},"False",[213,431,277],{"class":222},[213,433,434,437,439,442,445],{"class":133,"line":241},[213,435,436],{"class":222},"qgs.setPrefixPath(",[213,438,245],{"class":244},[213,440,441],{"class":222},", ",[213,443,444],{"class":244},"True",[213,446,277],{"class":222},[213,448,449],{"class":133,"line":259},[213,450,451],{"class":222},"qgs.initQgis()\n",[213,453,454],{"class":133,"line":280},[213,455,238],{"emptyLinePlaceholder":237},[213,457,458,460,463,465],{"class":133,"line":300},[213,459,331],{"class":218},[213,461,462],{"class":222}," processing.core.Processing ",[213,464,219],{"class":218},[213,466,467],{"class":222}," Processing\n",[213,469,470,472],{"class":133,"line":305},[213,471,219],{"class":218},[213,473,474],{"class":222}," processing\n",[213,476,477],{"class":133,"line":323},[213,478,479],{"class":222},"Processing.initialize()\n",[213,481,482],{"class":133,"line":328},[213,483,238],{"emptyLinePlaceholder":237},[213,485,486,488,491,494,497,500],{"class":133,"line":342},[213,487,345],{"class":244},[213,489,490],{"class":222},"(",[213,492,493],{"class":244},"len",[213,495,496],{"class":222},"(QgsApplication.processingRegistry().algorithms()), ",[213,498,499],{"class":251},"\"algorithms available\"",[213,501,277],{"class":222},[14,503,504,506,507,510,511,514,515,517,518,522],{},[175,505,376],{}," ",[210,508,509],{},"QgsApplication([], False)"," creates the application with no arguments and no GUI. ",[210,512,513],{},"initQgis()"," loads the providers — without it, every layer you create is invalid for reasons the error message does not explain. Processing needs its own initialisation in a standalone context, and after it the registry reports several hundred algorithms; a count of zero means the plugins folder is missing from the path. Run this cell exactly once per kernel: calling ",[210,516,513],{}," twice is undefined behaviour, so guard it or simply restart the kernel when things get confused. The equivalent standalone-script setup is covered in ",[21,519,521],{"href":520},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","Running Python Scripts Outside QGIS Desktop",".",[164,524,526],{"id":525},"render-a-map-into-a-cell","Render a map into a cell",[14,528,529],{},"The payoff of a notebook is seeing the result. Rendering to an image and displaying it inline takes a dozen lines and works with no display attached.",[203,531,533],{"className":205,"code":532,"language":207,"meta":208,"style":208},"from IPython.display import Image\nfrom qgis.core import (QgsVectorLayer, QgsProject, QgsMapSettings,\n                       QgsMapRendererParallelJob)\nfrom qgis.PyQt.QtCore import QSize\nfrom qgis.PyQt.QtGui import QColor\n\nlayer = QgsVectorLayer(\"\u002Fdata\u002Fcity.gpkg|layername=parcels\", \"Parcels\", \"ogr\")\nQgsProject.instance().addMapLayer(layer)\n\nsettings = QgsMapSettings()\nsettings.setLayers([layer])\nsettings.setBackgroundColor(QColor(\"white\"))\nsettings.setOutputSize(QSize(800, 500))\nsettings.setExtent(layer.extent())\n\njob = QgsMapRendererParallelJob(settings)\njob.start()\njob.waitForFinished()\njob.renderedImage().save(\"\u002Ftmp\u002Fmap.png\", \"PNG\")\n\nImage(\"\u002Ftmp\u002Fmap.png\")\n",[210,534,535,547,558,563,575,587,591,616,621,625,635,640,652,668,674,679,690,696,702,718,723],{"__ignoreMap":208},[213,536,537,539,542,544],{"class":133,"line":215},[213,538,331],{"class":218},[213,540,541],{"class":222}," IPython.display ",[213,543,219],{"class":218},[213,545,546],{"class":222}," Image\n",[213,548,549,551,553,555],{"class":133,"line":226},[213,550,331],{"class":218},[213,552,334],{"class":222},[213,554,219],{"class":218},[213,556,557],{"class":222}," (QgsVectorLayer, QgsProject, QgsMapSettings,\n",[213,559,560],{"class":133,"line":234},[213,561,562],{"class":222},"                       QgsMapRendererParallelJob)\n",[213,564,565,567,570,572],{"class":133,"line":241},[213,566,331],{"class":218},[213,568,569],{"class":222}," qgis.PyQt.QtCore ",[213,571,219],{"class":218},[213,573,574],{"class":222}," QSize\n",[213,576,577,579,582,584],{"class":133,"line":259},[213,578,331],{"class":218},[213,580,581],{"class":222}," qgis.PyQt.QtGui ",[213,583,219],{"class":218},[213,585,586],{"class":222}," QColor\n",[213,588,589],{"class":133,"line":280},[213,590,238],{"emptyLinePlaceholder":237},[213,592,593,596,598,601,604,606,609,611,614],{"class":133,"line":300},[213,594,595],{"class":222},"layer ",[213,597,317],{"class":218},[213,599,600],{"class":222}," QgsVectorLayer(",[213,602,603],{"class":251},"\"\u002Fdata\u002Fcity.gpkg|layername=parcels\"",[213,605,441],{"class":222},[213,607,608],{"class":251},"\"Parcels\"",[213,610,441],{"class":222},[213,612,613],{"class":251},"\"ogr\"",[213,615,277],{"class":222},[213,617,618],{"class":133,"line":305},[213,619,620],{"class":222},"QgsProject.instance().addMapLayer(layer)\n",[213,622,623],{"class":133,"line":323},[213,624,238],{"emptyLinePlaceholder":237},[213,626,627,630,632],{"class":133,"line":328},[213,628,629],{"class":222},"settings ",[213,631,317],{"class":218},[213,633,634],{"class":222}," QgsMapSettings()\n",[213,636,637],{"class":133,"line":342},[213,638,639],{"class":222},"settings.setLayers([layer])\n",[213,641,643,646,649],{"class":133,"line":642},12,[213,644,645],{"class":222},"settings.setBackgroundColor(QColor(",[213,647,648],{"class":251},"\"white\"",[213,650,651],{"class":222},"))\n",[213,653,655,658,661,663,666],{"class":133,"line":654},13,[213,656,657],{"class":222},"settings.setOutputSize(QSize(",[213,659,660],{"class":244},"800",[213,662,441],{"class":222},[213,664,665],{"class":244},"500",[213,667,651],{"class":222},[213,669,671],{"class":133,"line":670},14,[213,672,673],{"class":222},"settings.setExtent(layer.extent())\n",[213,675,677],{"class":133,"line":676},15,[213,678,238],{"emptyLinePlaceholder":237},[213,680,682,685,687],{"class":133,"line":681},16,[213,683,684],{"class":222},"job ",[213,686,317],{"class":218},[213,688,689],{"class":222}," QgsMapRendererParallelJob(settings)\n",[213,691,693],{"class":133,"line":692},17,[213,694,695],{"class":222},"job.start()\n",[213,697,699],{"class":133,"line":698},18,[213,700,701],{"class":222},"job.waitForFinished()\n",[213,703,705,708,711,713,716],{"class":133,"line":704},19,[213,706,707],{"class":222},"job.renderedImage().save(",[213,709,710],{"class":251},"\"\u002Ftmp\u002Fmap.png\"",[213,712,441],{"class":222},[213,714,715],{"class":251},"\"PNG\"",[213,717,277],{"class":222},[213,719,721],{"class":133,"line":720},20,[213,722,238],{"emptyLinePlaceholder":237},[213,724,726,729,731],{"class":133,"line":725},21,[213,727,728],{"class":222},"Image(",[213,730,710],{"class":251},[213,732,277],{"class":222},[14,734,735,506,737,740,741,744,745,748],{},[175,736,376],{},[210,738,739],{},"QgsMapSettings"," describes what to draw, at what size and over what extent — it is the same object the canvas uses, which is why the output matches what QGIS desktop would show. ",[210,742,743],{},"QgsMapRendererParallelJob"," renders on worker threads; ",[210,746,747],{},"waitForFinished()"," blocks until it is done, which is what you want in a notebook where the next cell expects the file to exist. Saving to a file and displaying it is more reliable than converting the image in memory, and it leaves an artefact you can put in a report. Re-running the cell after changing the styling gives the map-adjust-look loop that makes notebooks worth using for cartography experiments.",[14,750,751],{},[29,752,755,758,761,764,771,774,780,784,788,791,795,798,801,805,808,813,817,820,824,828,831,836,841,846],{"viewBox":753,"role":32,"ariaLabel":754,"xmlns":34},"0 0 760 250","Cycle of a notebook workflow showing load, analyse, render, inspect and adjust, with the rendered map and the attribute table both visible in the document",[36,756,757],{},"The loop a notebook is good at",[40,759,760],{},"Load the data, run an analysis step, render the result as an image, inspect both the image and the attribute summary, then adjust a parameter and repeat. Each iteration is recorded in the document, so the reasoning and the outputs stay together rather than being lost in a console history.",[44,762],{"x":46,"y":46,"width":47,"height":763,"fill":49},"250",[51,765,766],{},[54,767,769],{"id":768,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"loopArrow",[63,770],{"d":65,"fill":105},[68,772,773],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Every iteration stays in the document",[44,775],{"x":776,"y":777,"width":778,"height":779,"rx":58,"fill":104,"stroke":105,"style":84},"24","64","140","60",[68,781,783],{"x":782,"y":136,"style":89,"fill":105,"textAnchor":74},"94","load",[68,785,787],{"x":782,"y":786,"style":94,"fill":66,"textAnchor":74},"110","layers and tables",[44,789],{"x":790,"y":777,"width":778,"height":779,"rx":58,"fill":82,"stroke":83,"style":84},"192",[68,792,794],{"x":793,"y":136,"style":89,"fill":83,"textAnchor":74},"262","analyse",[68,796,797],{"x":793,"y":786,"style":94,"fill":66,"textAnchor":74},"one algorithm",[44,799],{"x":800,"y":777,"width":778,"height":779,"rx":58,"fill":120,"stroke":121,"style":84},"360",[68,802,804],{"x":803,"y":136,"style":89,"fill":121,"textAnchor":74},"430","render",[68,806,807],{"x":803,"y":786,"style":94,"fill":66,"textAnchor":74},"inline image",[44,809],{"x":810,"y":777,"width":778,"height":779,"rx":58,"fill":811,"stroke":812,"style":84},"528","#fdf2e2","#b45309",[68,814,816],{"x":815,"y":136,"style":89,"fill":812,"textAnchor":74},"598","inspect",[68,818,819],{"x":815,"y":786,"style":94,"fill":66,"textAnchor":74},"map and numbers",[133,821],{"x1":822,"y1":782,"x2":152,"y2":782,"stroke":105,"style":823},"164","stroke-width:2;marker-end:url(#loopArrow)",[133,825],{"x1":826,"y1":782,"x2":827,"y2":782,"stroke":105,"style":823},"332","354",[133,829],{"x1":665,"y1":782,"x2":830,"y2":782,"stroke":105,"style":823},"522",[63,832],{"d":833,"fill":834,"stroke":105,"style":835},"M598 124 L598 168 L94 168 L94 130","none","stroke-width:2;stroke-dasharray:6 4;marker-end:url(#loopArrow)",[68,837,840],{"x":838,"y":839,"style":94,"fill":105,"textAnchor":74},"346","188","adjust a parameter and run it again",[44,842],{"x":87,"y":843,"width":146,"height":844,"rx":58,"fill":149,"stroke":98,"style":845},"204","34","stroke-width:1.5",[68,847,849],{"x":70,"y":848,"style":94,"fill":66,"textAnchor":74},"226","Restart and run all before sharing — hidden state is the notebook's one real hazard",[164,851,853],{"id":852},"combine-pyqgis-with-the-data-stack","Combine PyQGIS with the data stack",[14,855,856],{},"Attributes are more convenient as a data frame, and getting them there is a comprehension:",[203,858,860],{"className":205,"code":859,"language":207,"meta":208,"style":208},"import pandas as pd\n\nrecords = [\n    {**dict(zip(layer.fields().names(), feature.attributes())),\n     \"area_m2\": feature.geometry().area()}\n    for feature in layer.getFeatures()\n]\nframe = pd.DataFrame.from_records(records)\nframe.groupby(\"ward\")[\"area_m2\"].sum().sort_values(ascending=False).head()\n",[210,861,862,875,879,889,908,916,930,935,945],{"__ignoreMap":208},[213,863,864,866,869,872],{"class":133,"line":215},[213,865,219],{"class":218},[213,867,868],{"class":222}," pandas ",[213,870,871],{"class":218},"as",[213,873,874],{"class":222}," pd\n",[213,876,877],{"class":133,"line":226},[213,878,238],{"emptyLinePlaceholder":237},[213,880,881,884,886],{"class":133,"line":234},[213,882,883],{"class":222},"records ",[213,885,317],{"class":218},[213,887,888],{"class":222}," [\n",[213,890,891,894,897,900,902,905],{"class":133,"line":241},[213,892,893],{"class":222},"    {",[213,895,896],{"class":218},"**",[213,898,899],{"class":244},"dict",[213,901,490],{"class":222},[213,903,904],{"class":244},"zip",[213,906,907],{"class":222},"(layer.fields().names(), feature.attributes())),\n",[213,909,910,913],{"class":133,"line":259},[213,911,912],{"class":251},"     \"area_m2\"",[213,914,915],{"class":222},": feature.geometry().area()}\n",[213,917,918,921,924,927],{"class":133,"line":280},[213,919,920],{"class":218},"    for",[213,922,923],{"class":222}," feature ",[213,925,926],{"class":218},"in",[213,928,929],{"class":222}," layer.getFeatures()\n",[213,931,932],{"class":133,"line":300},[213,933,934],{"class":222},"]\n",[213,936,937,940,942],{"class":133,"line":305},[213,938,939],{"class":222},"frame ",[213,941,317],{"class":218},[213,943,944],{"class":222}," pd.DataFrame.from_records(records)\n",[213,946,947,950,953,956,959,962,966,968,970],{"class":133,"line":323},[213,948,949],{"class":222},"frame.groupby(",[213,951,952],{"class":251},"\"ward\"",[213,954,955],{"class":222},")[",[213,957,958],{"class":251},"\"area_m2\"",[213,960,961],{"class":222},"].sum().sort_values(",[213,963,965],{"class":964},"s9osk","ascending",[213,967,317],{"class":218},[213,969,429],{"class":244},[213,971,972],{"class":222},").head()\n",[14,974,975,977,978,522],{},[175,976,376],{}," Zipping field names with attribute values gives a dictionary per feature, and adding a computed geometry column in the same step avoids a second pass. From there the whole of pandas applies: grouping, joining to a spreadsheet, plotting a distribution. The traffic works both ways — a result computed in pandas can be written back as a new field, or exported to CSV and joined in QGIS. For large layers, narrow the read with a feature request first rather than materialising every attribute, following ",[21,979,981],{"href":980},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002F","Speed Up Feature Iteration with QgsFeatureRequest",[14,983,984],{},[29,985,988,991,994,997,1004,1007,1011,1014,1017,1021,1025,1029,1032,1036,1039,1042,1045,1048,1054,1060,1063],{"viewBox":986,"role":32,"ariaLabel":987,"xmlns":34},"0 0 760 248","Diagram of data moving between PyQGIS layers and a pandas data frame in a notebook, with geometry staying on the QGIS side and computed values returning as fields",[36,989,990],{},"Where each half of the work belongs",[40,992,993],{},"Geometry, coordinate systems, spatial predicates and rendering stay on the PyQGIS side, where the algorithms are. Grouping, joining to spreadsheets, statistics and plotting happen on the data frame side. Attributes cross from layer to frame as records, and computed values cross back as a new field or a join key.",[44,995],{"x":46,"y":46,"width":47,"height":996,"fill":49},"248",[51,998,999],{},[54,1000,1002],{"id":1001,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"jupFlowArrow",[63,1003],{"d":65,"fill":66},[68,1005,1006],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Let each side do what it is good at",[44,1008],{"x":78,"y":1009,"width":1010,"height":822,"rx":148,"fill":104,"stroke":105,"style":106},"56","280",[68,1012,1013],{"x":145,"y":147,"style":153,"fill":105,"textAnchor":74},"PyQGIS side",[68,1015,1016],{"x":145,"y":786,"style":94,"fill":66,"textAnchor":74},"geometry and predicates",[68,1018,1020],{"x":145,"y":1019,"style":94,"fill":66,"textAnchor":74},"134","coordinate transforms",[68,1022,1024],{"x":145,"y":1023,"style":94,"fill":66,"textAnchor":74},"158","Processing algorithms",[68,1026,1028],{"x":145,"y":1027,"style":94,"fill":66,"textAnchor":74},"182","rendering to an image",[44,1030],{"x":1031,"y":1009,"width":1010,"height":822,"rx":148,"fill":82,"stroke":83,"style":106},"460",[68,1033,1035],{"x":1034,"y":147,"style":153,"fill":83,"textAnchor":74},"600","data frame side",[68,1037,1038],{"x":1034,"y":786,"style":94,"fill":66,"textAnchor":74},"grouping and pivoting",[68,1040,1041],{"x":1034,"y":1019,"style":94,"fill":66,"textAnchor":74},"joins to spreadsheets",[68,1043,1044],{"x":1034,"y":1023,"style":94,"fill":66,"textAnchor":74},"statistics and charts",[68,1046,1047],{"x":1034,"y":1027,"style":94,"fill":66,"textAnchor":74},"tables in the notebook",[133,1049],{"x1":1050,"y1":1051,"x2":1052,"y2":1051,"stroke":66,"style":1053},"300","106","454","stroke-width:2;marker-end:url(#jupFlowArrow)",[68,1055,1059],{"x":1056,"y":1057,"style":1058,"fill":98,"textAnchor":74},"378","98","text-anchor:middle;font-size:10px;font-family:sans-serif","attributes as records",[133,1061],{"x1":1052,"y1":1062,"x2":1050,"y2":1062,"stroke":66,"style":1053},"168",[68,1064,1065],{"x":1056,"y":145,"style":1058,"fill":98,"textAnchor":74},"results as a new field",[164,1067,1069],{"id":1068},"exit-cleanly-and-keep-notebooks-reproducible","Exit cleanly and keep notebooks reproducible",[14,1071,1072,1075,1076,1079],{},[210,1073,1074],{},"qgs.exitQgis()"," releases the providers and should be called at the end of a session, though in practice a notebook kernel is usually just restarted. The more important discipline is the one every notebook needs and GIS notebooks need more: ",[175,1077,1078],{},"restart the kernel and run all cells before sharing",". A notebook that only works because of a layer created in a cell that was later deleted is worse than a broken script, because it looks like it worked.",[14,1081,1082],{},"Two more habits pay off. Keep the setup — paths, initialisation, imports — in the first cell and nothing else, so the reproducibility question has one answer. And write outputs to files with explicit names rather than leaving them in memory, so the notebook produces artefacts a colleague can check without running anything.",[164,1084,1086],{"id":1085},"qgis-version-compatibility","QGIS version compatibility",[1088,1089,1090,1106],"table",{},[1091,1092,1093],"thead",{},[1094,1095,1096,1100,1103],"tr",{},[1097,1098,1099],"th",{},"QGIS version",[1097,1101,1102],{},"Python",[1097,1104,1105],{},"Notes",[1107,1108,1109,1121,1131,1142],"tbody",{},[1094,1110,1111,1115,1118],{},[1112,1113,1114],"td",{},"3.22 LTR",[1112,1116,1117],{},"3.9",[1112,1119,1120],{},"Works; the kernel's Python must also be 3.9.",[1094,1122,1123,1126,1128],{},[1112,1124,1125],{},"3.28 LTR",[1112,1127,1117],{},[1112,1129,1130],{},"Identical.",[1094,1132,1133,1136,1139],{},[1112,1134,1135],{},"3.34 LTR",[1112,1137,1138],{},"3.12",[1112,1140,1141],{},"Baseline for this page.",[1094,1143,1144,1147,1149],{},[1112,1145,1146],{},"3.40 \u002F 3.44",[1112,1148,1138],{},[1112,1150,1151],{},"Identical setup; no notebook-specific changes.",[14,1153,1154],{},"The binding constraint is always the Python minor version: PyQGIS is a compiled extension built for one version, and a 3.12 build cannot be imported by a 3.10 kernel. Where your data-science environment is pinned elsewhere, run QGIS work in its own kernel and exchange files.",[164,1156,1158],{"id":1157},"troubleshooting","Troubleshooting",[169,1160,1161,1169,1183,1200,1208,1218],{},[172,1162,1163,1168],{},[175,1164,1165,522],{},[210,1166,1167],{},"ModuleNotFoundError: qgis"," The bindings folder is not on the path, or the kernel's Python version does not match QGIS's.",[172,1170,1171,1179,1180,522],{},[175,1172,1173,1176,1177,522],{},[210,1174,1175],{},"ImportError"," for ",[210,1178,380],{}," The QGIS plugins folder is missing from ",[210,1181,1182],{},"sys.path",[172,1184,1185,1188,1189,1192,1193,1196,1197,522],{},[175,1186,1187],{},"A display error on a headless machine."," Set ",[210,1190,1191],{},"QT_QPA_PLATFORM=offscreen"," before importing anything from ",[210,1194,1195],{},"qgis"," or ",[210,1198,1199],{},"PyQt",[172,1201,1202,506,1205,1207],{},[175,1203,1204],{},"Layers are always invalid.",[210,1206,513],{}," was never called, or the prefix path is wrong so providers were not found.",[172,1209,1210,1213,1214,1217],{},[175,1211,1212],{},"The kernel dies when a cell runs."," Usually a second ",[210,1215,1216],{},"QgsApplication",", or a mismatched GDAL between the environment and QGIS. Restart and initialise once.",[172,1219,1220,506,1226,1229],{},[175,1221,1222,1225],{},[210,1223,1224],{},"processingRegistry().algorithms()"," is empty.",[210,1227,1228],{},"Processing.initialize()"," was not called, or the native provider is not registered.",[164,1231,1233],{"id":1232},"conclusion","Conclusion",[14,1235,1236,1237,1239],{},"Point the kernel at the QGIS Python libraries and the plugins folder, set the offscreen platform before Qt loads, create and initialise one ",[210,1238,1216],{},", and initialise Processing. From there PyQGIS is an ordinary library: build layers, run algorithms, render maps to inline images and move attributes into a data frame. Keep the setup in the first cell, and always restart-and-run-all before sharing.",[164,1241,1243],{"id":1242},"frequently-asked-questions","Frequently Asked Questions",[14,1245,1246,1249],{},[175,1247,1248],{},"Should I install Jupyter into the QGIS Python or the other way round?","\nInstalling Jupyter into the QGIS Python is far simpler and avoids all path manipulation. Reach for the other arrangement only when the notebook environment has dependencies you cannot install alongside QGIS.",[14,1251,1252,1255,1256,1258],{},[175,1253,1254],{},"Can I use the map canvas in a notebook?","\nNo — the canvas is a GUI widget. Render with ",[210,1257,739],{}," and a renderer job instead, which produces the same output as an image.",[14,1260,1261,1264],{},[175,1262,1263],{},"Does this work in JupyterLab and VS Code notebooks?","\nYes. The kernel is what matters; the front end is irrelevant.",[14,1266,1267,1270,1271,522],{},[175,1268,1269],{},"Can I run this on a server with no display?","\nYes, that is exactly what the offscreen platform is for. It is the same arrangement as ",[21,1272,1274],{"href":1273},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Frun-pyqgis-in-docker-container\u002F","Run PyQGIS in a Docker Container",[14,1276,1277,1280],{},[175,1278,1279],{},"How do I show an interactive map?","\nExport the layer to GeoJSON and use a web-mapping library in the notebook. PyQGIS renders images; interactivity comes from the notebook side.",[14,1282,1283,1286],{},[175,1284,1285],{},"Is a notebook a good place for production automation?","\nNo. Notebooks are for exploration; when the workflow settles, move it into a script that can be scheduled and tested.",[164,1288,1290],{"id":1289},"related","Related",[169,1292,1293,1298,1302,1306,1312],{},[172,1294,1295,1297],{},[21,1296,24],{"href":23}," — the guide this recipe belongs to",[172,1299,1300],{},[21,1301,521],{"href":520},[172,1303,1304],{},[21,1305,200],{"href":199},[172,1307,1308],{},[21,1309,1311],{"href":1310},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Frender-layer-to-image-without-gui-pyqgis\u002F","Render a Layer to an Image Without the GUI",[172,1313,1314],{},[21,1315,1317],{"href":1316},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","Headless QGIS and Server Automation",[1319,1320,1321],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}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":208,"searchDepth":226,"depth":226,"links":1323},[1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334],{"id":166,"depth":226,"text":167},{"id":192,"depth":226,"text":193},{"id":396,"depth":226,"text":397},{"id":525,"depth":226,"text":526},{"id":852,"depth":226,"text":853},{"id":1068,"depth":226,"text":1069},{"id":1085,"depth":226,"text":1086},{"id":1157,"depth":226,"text":1158},{"id":1232,"depth":226,"text":1233},{"id":1242,"depth":226,"text":1243},{"id":1289,"depth":226,"text":1290},"Run QGIS from a notebook — point the kernel at the QGIS libraries, initialise QgsApplication offscreen, render a map to an inline image, and combine PyQGIS with pandas without breaking either environment.","md",{"slug":1338,"type":1339,"breadcrumb":1340,"datePublished":1341,"dateModified":1341},"use-pyqgis-in-jupyter-notebook","article","PyQGIS in Jupyter","2026-08-15","\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Fuse-pyqgis-in-jupyter-notebook",{"title":5,"description":1335},"pyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Fuse-pyqgis-in-jupyter-notebook\u002Findex","t5TqrLQ677oKvIOEpL5tS4bLriMTQVJEBFxpAqPz5q4",1786789584631]