[{"data":1,"prerenderedAt":1672},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop":3},{"id":4,"title":5,"body":6,"description":1661,"extension":1662,"meta":1663,"navigation":381,"path":1668,"seo":1669,"stem":1670,"__hash__":1671},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002Findex.md","Running Python Scripts Outside QGIS Desktop",{"type":7,"value":8,"toc":1647},"minimark",[9,13,34,58,69,269,274,277,328,332,347,767,787,792,810,860,868,872,875,949,959,963,970,1051,1055,1062,1171,1175,1178,1310,1313,1317,1320,1487,1491,1525,1529,1549,1564,1573,1592,1610,1614,1643],[10,11,5],"h1",{"id":12},"running-python-scripts-outside-qgis-desktop",[14,15,16],"p",{},[17,18,19,24,25,24,29,33],"em",{},[20,21,23],"a",{"href":22},"\u002F","Home"," → ",[20,26,28],{"href":27},"\u002Fpyqgis-fundamentals-environment-setup\u002F","PyQGIS Fundamentals & Environment Setup",[20,30,32],{"href":31},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002F","Virtual Environments for QGIS and PyQGIS"," → Running Scripts Outside QGIS",[14,35,36,37,41,42,45,46,49,50,53,54,57],{},"Running PyQGIS from a plain terminal, a cron job, or a CI\u002FCD runner means doing manually what the QGIS application does for you at startup: locating the bindings, wiring up their compiled C++ dependencies, and standing up an application context. To run a Python script outside QGIS desktop you must bootstrap the environment before importing any ",[38,39,40],"code",{},"qgis"," module — set ",[38,43,44],{},"QGIS_PREFIX_PATH"," and ",[38,47,48],{},"PATH",", extend ",[38,51,52],{},"sys.path",", then call ",[38,55,56],{},"QgsApplication.initQgis()"," in headless mode. Done correctly, you keep the full spatial engine — vector and raster APIs, the GDAL\u002FOGR drivers, and the Processing framework — with none of the GUI overhead.",[14,59,60,61,63,64,68],{},"This page is the general launch pattern for standalone execution: scheduled exports, server-side geoprocessing, batch pipelines, and automated tests. It builds directly on the isolation strategy covered in ",[20,62,32],{"href":31},", and shares its bootstrap block with ",[20,65,67],{"href":66},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002Ffixing-pyqgis-module-import-errors\u002F","Fixing PyQGIS Module Import Errors"," — the difference is emphasis. Here the goal is a reliable, repeatable headless launch rather than a one-off diagnosis.",[70,71,76,80,84,91,98,107,111,115,121,126,130,136,142,145,150,153,157,162,167,169,172,176,180,184,186,188,190,195,199,203,208,213,217,221,226,229,232,234,236,238,241,244,249,254],"svg",{"viewBox":72,"role":73,"ariaLabel":74,"xmlns":75},"0 0 760 524","img","Comparison of how the QGIS desktop launch configures the environment automatically versus how a standalone interpreter must inject the same three variables manually before importing qgis","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[77,78,79],"title",{},"Desktop launch versus a standalone headless launch",[81,82,83],"desc",{},"When QGIS desktop starts, the application automatically sets QGIS_PREFIX_PATH, PATH and sys.path, imports the bindings, and opens the GUI event loop with iface ready. A standalone interpreter runs the same three variables manually before any qgis import, then constructs QgsApplication with the GUI disabled and calls initQgis to reach the full vector, raster and Processing engine headlessly, before shutting down with exitQgis.",[85,86],"rect",{"x":87,"y":87,"width":88,"height":89,"fill":90},"0","760","524","#f6f3ea",[85,92],{"x":93,"y":94,"width":95,"height":93,"rx":96,"fill":97},"40","30","300","8","#2563eb",[99,100,106],"text",{"x":101,"y":102,"style":103,"fill":104,"textAnchor":105},"190","55","text-anchor:middle;font-family:sans-serif;font-size:15px;font-weight:bold","#ffffff","middle","QGIS Desktop launch",[85,108],{"x":109,"y":94,"width":95,"height":93,"rx":96,"fill":110},"420","#15803d",[99,112,114],{"x":113,"y":102,"style":103,"fill":104,"textAnchor":105},"570","Standalone interpreter (this page)",[85,116],{"x":93,"y":117,"width":95,"height":118,"rx":96,"fill":119,"stroke":97,"style":120},"82","58","#fffdf7","stroke-width:2.5",[99,122,125],{"x":101,"y":123,"style":124,"fill":97,"textAnchor":105},"116","text-anchor:middle;font-family:sans-serif;font-size:13px;font-weight:bold","QGIS application starts",[85,127],{"x":93,"y":128,"width":95,"height":129,"rx":96,"fill":119,"stroke":97,"style":120},"156","62",[99,131,135],{"x":101,"y":132,"style":133,"fill":134,"textAnchor":105},"181","text-anchor:middle;font-family:sans-serif;font-size:12.5px;font-weight:bold","#17211d","App auto-sets the 3 variables",[99,137,141],{"x":101,"y":138,"style":139,"fill":140,"textAnchor":105},"201","text-anchor:middle;font-family:monospace;font-size:10.5px","#2f3b35","QGIS_PREFIX_PATH · PATH · sys.path",[85,143],{"x":93,"y":144,"width":95,"height":118,"rx":96,"fill":119,"stroke":97,"style":120},"234",[99,146,149],{"x":101,"y":147,"style":148,"fill":134,"textAnchor":105},"268","text-anchor:middle;font-family:monospace;font-size:13px;font-weight:bold","import qgis",[85,151],{"x":93,"y":152,"width":95,"height":118,"rx":96,"fill":119,"stroke":97,"style":120},"308",[99,154,156],{"x":101,"y":155,"style":133,"fill":97,"textAnchor":105},"332","GUI window + iface ready",[99,158,161],{"x":101,"y":159,"style":160,"fill":140,"textAnchor":105},"351","text-anchor:middle;font-family:sans-serif;font-size:11px","interactive desktop",[99,163,166],{"x":101,"y":164,"style":165,"fill":140,"textAnchor":105},"392","text-anchor:middle;font-family:sans-serif;font-size:11px;font-style:italic","Everything above is automatic",[85,168],{"x":109,"y":117,"width":95,"height":118,"rx":96,"fill":119,"stroke":110,"style":120},[99,170,171],{"x":113,"y":123,"style":148,"fill":134,"textAnchor":105},"python script.py",[85,173],{"x":109,"y":128,"width":95,"height":129,"rx":96,"fill":119,"stroke":174,"style":175},"#b45309","stroke-width:3",[99,177,179],{"x":113,"y":178,"style":133,"fill":174,"textAnchor":105},"180","You inject the same 3 variables",[99,181,183],{"x":113,"y":182,"style":160,"fill":140,"textAnchor":105},"200","before any qgis import — order matters",[85,185],{"x":109,"y":144,"width":95,"height":118,"rx":96,"fill":119,"stroke":110,"style":120},[99,187,149],{"x":113,"y":147,"style":148,"fill":134,"textAnchor":105},[85,189],{"x":109,"y":152,"width":95,"height":118,"rx":96,"fill":119,"stroke":110,"style":120},[99,191,194],{"x":113,"y":192,"style":193,"fill":134,"textAnchor":105},"331","text-anchor:middle;font-family:monospace;font-size:12px;font-weight:bold","QgsApplication([], False)",[99,196,198],{"x":113,"y":197,"style":160,"fill":140,"textAnchor":105},"350","initQgis() — GUI disabled",[85,200],{"x":109,"y":201,"width":95,"height":129,"rx":96,"fill":202,"stroke":110,"style":120},"382","#26322d",[99,204,207],{"x":113,"y":205,"style":133,"fill":206,"textAnchor":105},"407","#d9f99d","Vector · Raster · Processing",[99,209,212],{"x":113,"y":210,"style":160,"fill":211,"textAnchor":105},"427","#b6e39a","full engine, no GUI overhead",[85,214],{"x":109,"y":215,"width":95,"height":216,"rx":96,"fill":119,"stroke":110,"style":120},"460","46",[99,218,220],{"x":113,"y":219,"style":193,"fill":134,"textAnchor":105},"488","exitQgis()",[222,223],"line",{"x1":101,"y1":224,"x2":101,"y2":128,"stroke":140,"style":225},"140","stroke-width:2;marker-end:url(#so-arr)",[222,227],{"x1":101,"y1":228,"x2":101,"y2":144,"stroke":140,"style":225},"218",[222,230],{"x1":101,"y1":231,"x2":101,"y2":152,"stroke":140,"style":225},"292",[222,233],{"x1":113,"y1":224,"x2":113,"y2":128,"stroke":140,"style":225},[222,235],{"x1":113,"y1":228,"x2":113,"y2":144,"stroke":140,"style":225},[222,237],{"x1":113,"y1":231,"x2":113,"y2":152,"stroke":140,"style":225},[222,239],{"x1":113,"y1":240,"x2":113,"y2":201,"stroke":140,"style":225},"366",[222,242],{"x1":113,"y1":243,"x2":113,"y2":215,"stroke":140,"style":225},"444",[222,245],{"x1":246,"y1":247,"x2":109,"y2":247,"stroke":174,"style":248},"340","187","stroke-width:2;stroke-dasharray:5 4",[99,250,253],{"x":251,"y":178,"style":252,"fill":174,"textAnchor":105},"380","text-anchor:middle;font-family:sans-serif;font-size:10px;font-weight:bold","same vars",[255,256,257],"defs",{},[258,259,265],"marker",{"id":260,"markerWidth":261,"markerHeight":261,"refX":96,"refY":262,"orient":263,"markerUnits":264},"so-arr","10","3","auto","strokeWidth",[266,267],"path",{"d":268,"fill":140},"M0,0 L8,3 L0,6 Z",[270,271,273],"h2",{"id":272},"prerequisites","Prerequisites",[14,275,276],{},"Before you run a standalone script, confirm the following so you are configuring the right layer of the stack:",[278,279,280,288,294,304,310],"ul",{},[281,282,283,287],"li",{},[284,285,286],"strong",{},"A working QGIS install, with its exact version noted."," Check Help → About. Paths differ between a Windows Standalone build, OSGeo4W, a macOS bundle, and a Linux package.",[281,289,290,293],{},[284,291,292],{},"The bundled Python version."," PyQGIS bindings are compiled against one specific Python ABI. QGIS 3.34 and later — including the 3.44 LTR — ship Python 3.12, while the older 3.28 LTR shipped Python 3.9. Your standalone interpreter must match this minor version.",[281,295,296,299,300,303],{},[284,297,298],{},"A 64-bit interpreter."," Modern PyQGIS is strictly 64-bit; a 32-bit Python will fail to load ",[38,301,302],{},"_core",".",[281,305,306,309],{},[284,307,308],{},"An isolated environment for reproducibility."," For anything you intend to deploy, run inside a dedicated virtual environment or Conda environment rather than the system Python, so QGIS libraries resolve identically across development and production machines.",[281,311,312,315,316,319,320,323,324,327],{},[284,313,314],{},"The ability to print your environment."," You should be able to inspect ",[38,317,318],{},"sys.executable",", ",[38,321,322],{},"sys.version",", and ",[38,325,326],{},"os.environ"," from the interpreter that runs the job — most standalone failures are a mismatched interpreter, not a code bug.",[270,329,331],{"id":330},"the-standalone-bootstrap-recipe","The Standalone Bootstrap Recipe",[14,333,334,335,338,339,341,342,45,344,346],{},"Inject the QGIS paths ",[17,336,337],{},"before"," any ",[38,340,40],{}," import. Import order matters: the compiled libraries are resolved against ",[38,343,48],{},[38,345,52],{}," at import time, so setting these variables afterwards has no effect. Place this block at the very top of your script.",[348,349,354],"pre",{"className":350,"code":351,"language":352,"meta":353,"style":353},"language-python shiki shiki-themes github-dark","import os\nimport sys\n\n# 1. Set the QGIS installation root (adjust per OS and install type)\nQGIS_PREFIX = r\"C:\\Program Files\\QGIS 3.44\\apps\\qgis-ltr\"  # Windows Standalone LTR\n# QGIS_PREFIX = r\"C:\\OSGeo4W\\apps\\qgis-ltr\"                # Windows OSGeo4W\n# QGIS_PREFIX = \"\u002FApplications\u002FQGIS.app\u002FContents\u002FMacOS\"    # macOS bundle\n# QGIS_PREFIX = \"\u002Fusr\"                                     # Linux (Debian\u002FUbuntu)\n\n# 2. Inject QGIS paths BEFORE importing qgis modules\nsys.path.insert(0, os.path.join(QGIS_PREFIX, \"python\"))\nsys.path.insert(0, os.path.join(QGIS_PREFIX, \"python\", \"plugins\"))\n\nos.environ[\"QGIS_PREFIX_PATH\"] = QGIS_PREFIX\nos.environ[\"PATH\"] = os.path.join(QGIS_PREFIX, \"bin\") + os.pathsep + os.environ.get(\"PATH\", \"\")\n\nfrom qgis.core import QgsApplication, QgsVectorLayer\n\n# 3. Initialize a headless QGIS application (False disables the GUI)\nQgsApplication.setPrefixPath(QGIS_PREFIX, True)\nqgs = QgsApplication([], False)\nqgs.initQgis()\n\n# 4. Run your GIS logic\nlayer = QgsVectorLayer(\"data\u002Froads.shp\", \"roads\", \"ogr\")\nif layer.isValid():\n    print(f\"Loaded {layer.featureCount()} features\")\nelse:\n    print(\"Layer failed to load. Check the path and OGR drivers.\")\n\n# 5. Clean shutdown\nqgs.exitQgis()\n","python","",[38,355,356,368,376,383,390,446,452,458,464,469,475,496,518,523,541,587,592,606,611,617,632,648,654,659,665,691,700,729,738,750,755,761],{"__ignoreMap":353},[357,358,360,364],"span",{"class":222,"line":359},1,[357,361,363],{"class":362},"snl16","import",[357,365,367],{"class":366},"s95oV"," os\n",[357,369,371,373],{"class":222,"line":370},2,[357,372,363],{"class":362},[357,374,375],{"class":366}," sys\n",[357,377,379],{"class":222,"line":378},3,[357,380,382],{"emptyLinePlaceholder":381},true,"\n",[357,384,386],{"class":222,"line":385},4,[357,387,389],{"class":388},"sjoCn","# 1. Set the QGIS installation root (adjust per OS and install type)\n",[357,391,393,397,400,403,407,411,415,418,421,424,426,429,432,435,438,441,443],{"class":222,"line":392},5,[357,394,396],{"class":395},"sDLfK","QGIS_PREFIX",[357,398,399],{"class":362}," =",[357,401,402],{"class":362}," r",[357,404,406],{"class":405},"sU2Wk","\"",[357,408,410],{"class":409},"sns5M","C:",[357,412,414],{"class":413},"sRjNt","\\P",[357,416,417],{"class":409},"rogram Files",[357,419,420],{"class":413},"\\Q",[357,422,423],{"class":409},"GIS 3",[357,425,303],{"class":395},[357,427,428],{"class":409},"44",[357,430,431],{"class":413},"\\a",[357,433,434],{"class":409},"pps",[357,436,437],{"class":413},"\\q",[357,439,440],{"class":409},"gis-ltr",[357,442,406],{"class":405},[357,444,445],{"class":388},"  # Windows Standalone LTR\n",[357,447,449],{"class":222,"line":448},6,[357,450,451],{"class":388},"# QGIS_PREFIX = r\"C:\\OSGeo4W\\apps\\qgis-ltr\"                # Windows OSGeo4W\n",[357,453,455],{"class":222,"line":454},7,[357,456,457],{"class":388},"# QGIS_PREFIX = \"\u002FApplications\u002FQGIS.app\u002FContents\u002FMacOS\"    # macOS bundle\n",[357,459,461],{"class":222,"line":460},8,[357,462,463],{"class":388},"# QGIS_PREFIX = \"\u002Fusr\"                                     # Linux (Debian\u002FUbuntu)\n",[357,465,467],{"class":222,"line":466},9,[357,468,382],{"emptyLinePlaceholder":381},[357,470,472],{"class":222,"line":471},10,[357,473,474],{"class":388},"# 2. Inject QGIS paths BEFORE importing qgis modules\n",[357,476,478,481,483,486,488,490,493],{"class":222,"line":477},11,[357,479,480],{"class":366},"sys.path.insert(",[357,482,87],{"class":395},[357,484,485],{"class":366},", os.path.join(",[357,487,396],{"class":395},[357,489,319],{"class":366},[357,491,492],{"class":405},"\"python\"",[357,494,495],{"class":366},"))\n",[357,497,499,501,503,505,507,509,511,513,516],{"class":222,"line":498},12,[357,500,480],{"class":366},[357,502,87],{"class":395},[357,504,485],{"class":366},[357,506,396],{"class":395},[357,508,319],{"class":366},[357,510,492],{"class":405},[357,512,319],{"class":366},[357,514,515],{"class":405},"\"plugins\"",[357,517,495],{"class":366},[357,519,521],{"class":222,"line":520},13,[357,522,382],{"emptyLinePlaceholder":381},[357,524,526,529,532,535,538],{"class":222,"line":525},14,[357,527,528],{"class":366},"os.environ[",[357,530,531],{"class":405},"\"QGIS_PREFIX_PATH\"",[357,533,534],{"class":366},"] ",[357,536,537],{"class":362},"=",[357,539,540],{"class":395}," QGIS_PREFIX\n",[357,542,544,546,549,551,553,556,558,560,563,566,569,572,574,577,579,581,584],{"class":222,"line":543},15,[357,545,528],{"class":366},[357,547,548],{"class":405},"\"PATH\"",[357,550,534],{"class":366},[357,552,537],{"class":362},[357,554,555],{"class":366}," os.path.join(",[357,557,396],{"class":395},[357,559,319],{"class":366},[357,561,562],{"class":405},"\"bin\"",[357,564,565],{"class":366},") ",[357,567,568],{"class":362},"+",[357,570,571],{"class":366}," os.pathsep ",[357,573,568],{"class":362},[357,575,576],{"class":366}," os.environ.get(",[357,578,548],{"class":405},[357,580,319],{"class":366},[357,582,583],{"class":405},"\"\"",[357,585,586],{"class":366},")\n",[357,588,590],{"class":222,"line":589},16,[357,591,382],{"emptyLinePlaceholder":381},[357,593,595,598,601,603],{"class":222,"line":594},17,[357,596,597],{"class":362},"from",[357,599,600],{"class":366}," qgis.core ",[357,602,363],{"class":362},[357,604,605],{"class":366}," QgsApplication, QgsVectorLayer\n",[357,607,609],{"class":222,"line":608},18,[357,610,382],{"emptyLinePlaceholder":381},[357,612,614],{"class":222,"line":613},19,[357,615,616],{"class":388},"# 3. Initialize a headless QGIS application (False disables the GUI)\n",[357,618,620,623,625,627,630],{"class":222,"line":619},20,[357,621,622],{"class":366},"QgsApplication.setPrefixPath(",[357,624,396],{"class":395},[357,626,319],{"class":366},[357,628,629],{"class":395},"True",[357,631,586],{"class":366},[357,633,635,638,640,643,646],{"class":222,"line":634},21,[357,636,637],{"class":366},"qgs ",[357,639,537],{"class":362},[357,641,642],{"class":366}," QgsApplication([], ",[357,644,645],{"class":395},"False",[357,647,586],{"class":366},[357,649,651],{"class":222,"line":650},22,[357,652,653],{"class":366},"qgs.initQgis()\n",[357,655,657],{"class":222,"line":656},23,[357,658,382],{"emptyLinePlaceholder":381},[357,660,662],{"class":222,"line":661},24,[357,663,664],{"class":388},"# 4. Run your GIS logic\n",[357,666,668,671,673,676,679,681,684,686,689],{"class":222,"line":667},25,[357,669,670],{"class":366},"layer ",[357,672,537],{"class":362},[357,674,675],{"class":366}," QgsVectorLayer(",[357,677,678],{"class":405},"\"data\u002Froads.shp\"",[357,680,319],{"class":366},[357,682,683],{"class":405},"\"roads\"",[357,685,319],{"class":366},[357,687,688],{"class":405},"\"ogr\"",[357,690,586],{"class":366},[357,692,694,697],{"class":222,"line":693},26,[357,695,696],{"class":362},"if",[357,698,699],{"class":366}," layer.isValid():\n",[357,701,703,706,709,712,715,718,721,724,727],{"class":222,"line":702},27,[357,704,705],{"class":395},"    print",[357,707,708],{"class":366},"(",[357,710,711],{"class":362},"f",[357,713,714],{"class":405},"\"Loaded ",[357,716,717],{"class":395},"{",[357,719,720],{"class":366},"layer.featureCount()",[357,722,723],{"class":395},"}",[357,725,726],{"class":405}," features\"",[357,728,586],{"class":366},[357,730,732,735],{"class":222,"line":731},28,[357,733,734],{"class":362},"else",[357,736,737],{"class":366},":\n",[357,739,741,743,745,748],{"class":222,"line":740},29,[357,742,705],{"class":395},[357,744,708],{"class":366},[357,746,747],{"class":405},"\"Layer failed to load. Check the path and OGR drivers.\"",[357,749,586],{"class":366},[357,751,753],{"class":222,"line":752},30,[357,754,382],{"emptyLinePlaceholder":381},[357,756,758],{"class":222,"line":757},31,[357,759,760],{"class":388},"# 5. Clean shutdown\n",[357,762,764],{"class":222,"line":763},32,[357,765,766],{"class":366},"qgs.exitQgis()\n",[14,768,769,770,772,773,775,776,779,780,319,782,323,784,786],{},"The ",[38,771,645],{}," in ",[38,774,194],{}," is the whole point of a standalone run: it disables GUI initialization, so no windows or display server are required. That is what makes the script safe for cron, servers, and headless runners. Note that ",[38,777,778],{},"PYTHONHOME"," is deliberately left unset — forcing it makes the interpreter look for its standard library inside the QGIS prefix, which breaks virtual environments and can crash silently. Setting ",[38,781,44],{},[38,783,48],{},[38,785,52],{}," is enough.",[788,789,791],"h3",{"id":790},"adding-the-processing-framework","Adding the Processing framework",[14,793,794,797,798,801,802,805,806,809],{},[38,795,796],{},"QgsVectorLayer",", geometry, and CRS classes are available the moment ",[38,799,800],{},"initQgis()"," returns. The Processing framework's native algorithms are not — they live in a plugin that must be registered explicitly before ",[38,803,804],{},"processing.run()"," will resolve ",[38,807,808],{},"native:*"," algorithm IDs:",[348,811,813],{"className":350,"code":812,"language":352,"meta":353,"style":353},"from qgis.analysis import QgsNativeAlgorithms\nimport processing\nfrom processing.core.Processing import Processing\n\nProcessing.initialize()\nQgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())\n",[38,814,815,827,834,846,850,855],{"__ignoreMap":353},[357,816,817,819,822,824],{"class":222,"line":359},[357,818,597],{"class":362},[357,820,821],{"class":366}," qgis.analysis ",[357,823,363],{"class":362},[357,825,826],{"class":366}," QgsNativeAlgorithms\n",[357,828,829,831],{"class":222,"line":370},[357,830,363],{"class":362},[357,832,833],{"class":366}," processing\n",[357,835,836,838,841,843],{"class":222,"line":378},[357,837,597],{"class":362},[357,839,840],{"class":366}," processing.core.Processing ",[357,842,363],{"class":362},[357,844,845],{"class":366}," Processing\n",[357,847,848],{"class":222,"line":385},[357,849,382],{"emptyLinePlaceholder":381},[357,851,852],{"class":222,"line":392},[357,853,854],{"class":366},"Processing.initialize()\n",[357,856,857],{"class":222,"line":448},[357,858,859],{"class":366},"QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())\n",[14,861,862,863,867],{},"With the registry populated you can drive the same algorithms you use interactively — see ",[20,864,866],{"href":865},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-processing-algorithm-from-script\u002F","Run a Processing Algorithm from a Script"," for the call-and-parameters pattern that this bootstrap unlocks.",[788,869,871],{"id":870},"wiring-it-into-a-scheduler","Wiring it into a scheduler",[14,873,874],{},"For cron or Task Scheduler, do not rely on the shell's ambient environment. Wrap the call in a small launcher that exports the variables first, then invokes the interpreter, so the job runs identically whether a human or the scheduler triggers it:",[348,876,880],{"className":877,"code":878,"language":879,"meta":353,"style":353},"language-bash shiki shiki-themes github-dark","#!\u002Fusr\u002Fbin\u002Fenv bash\nexport QGIS_PREFIX_PATH=\u002Fusr\nexport QT_QPA_PLATFORM=offscreen        # headless Linux: no X display\nexport PATH=\"$QGIS_PREFIX_PATH\u002Fbin:$PATH\"\nexec \u002Fusr\u002Fbin\u002Fpython3 \u002Fopt\u002Fpipelines\u002Fstandalone_script.py\n","bash",[38,881,882,887,900,915,938],{"__ignoreMap":353},[357,883,884],{"class":222,"line":359},[357,885,886],{"class":388},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[357,888,889,892,895,897],{"class":222,"line":370},[357,890,891],{"class":362},"export",[357,893,894],{"class":366}," QGIS_PREFIX_PATH",[357,896,537],{"class":362},[357,898,899],{"class":366},"\u002Fusr\n",[357,901,902,904,907,909,912],{"class":222,"line":378},[357,903,891],{"class":362},[357,905,906],{"class":366}," QT_QPA_PLATFORM",[357,908,537],{"class":362},[357,910,911],{"class":366},"offscreen        ",[357,913,914],{"class":388},"# headless Linux: no X display\n",[357,916,917,919,922,924,926,929,932,935],{"class":222,"line":385},[357,918,891],{"class":362},[357,920,921],{"class":366}," PATH",[357,923,537],{"class":362},[357,925,406],{"class":405},[357,927,928],{"class":366},"$QGIS_PREFIX_PATH",[357,930,931],{"class":405},"\u002Fbin:",[357,933,934],{"class":366},"$PATH",[357,936,937],{"class":405},"\"\n",[357,939,940,943,946],{"class":222,"line":392},[357,941,942],{"class":395},"exec",[357,944,945],{"class":405}," \u002Fusr\u002Fbin\u002Fpython3",[357,947,948],{"class":405}," \u002Fopt\u002Fpipelines\u002Fstandalone_script.py\n",[14,950,951,952,955,956,303],{},"On a headless Linux server the ",[38,953,954],{},"QT_QPA_PLATFORM=offscreen"," export is essential — even in GUI-disabled mode Qt may try to open an X connection and abort with ",[38,957,958],{},"qt.qpa.xcb: could not connect to display",[270,960,962],{"id":961},"what-is-missing-without-the-desktop","What is missing without the desktop",[14,964,965,966,969],{},"A standalone script has all of ",[38,967,968],{},"qgis.core"," and none of the application. Knowing which side of that line a class falls on prevents most of the failures.",[14,971,972],{},[70,973,976,979,982,985,990,995,1001,1025,1029,1033],{"viewBox":974,"role":73,"ariaLabel":975,"xmlns":75},"0 0 760 244","Two columns dividing the API into what works headless — core classes and processing — and what does not: iface, the canvas, dialogs and message bars",[77,977,978],{},"What is available with no desktop running",[81,980,981],{},"Available headless: layers, geometry, coordinate transforms, processing algorithms, map settings and render jobs, and the expression engine. Not available: the iface object, the map canvas, dialogs and message bars, map tools, and the layer tree view. A note records that anything importing from qgis.gui should be treated as suspect.",[85,983],{"x":87,"y":87,"width":88,"height":984,"fill":90},"244",[99,986,989],{"x":251,"y":987,"style":988,"fill":134,"textAnchor":105},"26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","The dividing line runs between qgis.core and qgis.gui",[85,991],{"x":992,"y":216,"width":993,"height":994,"rx":261,"fill":119,"stroke":110,"style":120},"16","356","182",[99,996,1000],{"x":997,"y":998,"style":999,"fill":110,"textAnchor":105},"194","70","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","works headless",[1002,1003,1005,1009,1013,1017,1021],"g",{"style":1004},"font-size:11.5px;font-family:sans-serif",[99,1006,1008],{"x":428,"y":1007,"fill":140},"98","✓  QgsVectorLayer, QgsRasterLayer",[99,1010,1012],{"x":428,"y":1011,"fill":140},"124","✓  QgsGeometry, QgsCoordinateTransform",[99,1014,1016],{"x":428,"y":1015,"fill":140},"150","✓  processing.run()",[99,1018,1020],{"x":428,"y":1019,"fill":140},"176","✓  QgsMapSettings + render jobs",[99,1022,1024],{"x":428,"y":1023,"fill":140},"202","✓  the expression engine",[85,1026],{"x":1027,"y":216,"width":993,"height":994,"rx":261,"fill":119,"stroke":1028,"style":120},"388","#b91c1c",[99,1030,1032],{"x":1031,"y":998,"style":999,"fill":1028,"textAnchor":105},"566","not available",[1002,1034,1035,1039,1042,1045,1048],{"style":1004},[99,1036,1038],{"x":1037,"y":1007,"fill":140},"416","✗  iface — there is no application",[99,1040,1041],{"x":1037,"y":1011,"fill":140},"✗  QgsMapCanvas and map tools",[99,1043,1044],{"x":1037,"y":1015,"fill":140},"✗  dialogs and the message bar",[99,1046,1047],{"x":1037,"y":1019,"fill":140},"✗  the layer tree view",[99,1049,1050],{"x":1037,"y":1023,"fill":1028},"anything from qgis.gui is suspect",[270,1052,1054],{"id":1053},"loading-a-project-versus-building-one","Loading a project versus building one",[14,1056,1057,1058,1061],{},"Two shapes of standalone script exist, and choosing between them decides how much of the work lives in Python rather than in a ",[38,1059,1060],{},".qgz"," file someone can edit without you.",[14,1063,1064],{},[70,1065,1068,1071,1074,1077,1090,1093,1095,1098,1107,1113,1117,1123,1127,1132,1137,1140,1142,1145,1148,1152,1154,1157,1161,1165,1168],{"viewBox":1066,"role":73,"ariaLabel":1067,"xmlns":75},"0 0 760 236","A script that opens an existing project and reads its layers and styles, compared with one that constructs every layer and style in code",[77,1069,1070],{},"Open a project, or build everything in code",[81,1072,1073],{},"Opening an existing project inherits its layers, styles and layouts, so a cartographer can change the map without touching the script. Building everything in code gives full control and no external dependency, but every styling decision has to be expressed as Python and maintained there.",[85,1075],{"x":87,"y":87,"width":88,"height":1076,"fill":90},"236",[255,1078,1079],{},[258,1080,1086],{"id":1081,"viewBox":1082,"refX":96,"refY":1083,"markerWidth":1084,"markerHeight":1084,"orient":1085},"soloShapeArrow","0 0 10 10","5","7","auto-start-reverse",[266,1087],{"d":1088,"fill":1089},"M0 0 L10 5 L0 10 z","#0f766e",[99,1091,1092],{"x":251,"y":987,"style":988,"fill":134,"textAnchor":105},"Who owns the map design — the script, or a project file?",[85,1094],{"x":992,"y":216,"width":993,"height":1019,"rx":261,"fill":119,"stroke":110,"style":120},[99,1096,1097],{"x":997,"y":998,"style":999,"fill":110,"textAnchor":105},"read.setFileName(\"site.qgz\")",[85,1099],{"x":1100,"y":1101,"width":1102,"height":1103,"rx":1104,"fill":110,"fillOpacity":1105,"stroke":110,"style":1106},"48","90","120","52","6",0.14,"stroke-width:2",[99,1108,1112],{"x":1109,"y":1110,"style":1111,"fill":140,"textAnchor":105},"108","112","text-anchor:middle;font-size:10.5px;font-family:sans-serif","site.qgz",[99,1114,1116],{"x":1109,"y":1115,"style":1111,"fill":140,"textAnchor":105},"130","layers + styles",[85,1118],{"x":1119,"y":1101,"width":1120,"height":1103,"rx":1104,"fill":119,"stroke":1121,"style":1122},"220","128","#59645f","stroke-width:1.6",[99,1124,1126],{"x":1125,"y":1102,"style":1111,"fill":140,"textAnchor":105},"284","a short script",[222,1128],{"x1":1129,"y1":123,"x2":1130,"y2":123,"stroke":1089,"style":1131},"168","216","stroke-width:2;marker-end:url(#soloShapeArrow)",[99,1133,1136],{"x":997,"y":1134,"style":1135,"fill":110,"textAnchor":105},"172","text-anchor:middle;font-size:11px;font-family:sans-serif","a cartographer can change the map",[99,1138,1139],{"x":997,"y":997,"style":1135,"fill":140,"textAnchor":105},"without touching your code",[85,1141],{"x":1027,"y":216,"width":993,"height":1019,"rx":261,"fill":119,"stroke":97,"style":120},[99,1143,1144],{"x":1031,"y":998,"style":999,"fill":97,"textAnchor":105},"build it all in code",[85,1146],{"x":109,"y":1101,"width":1120,"height":1103,"rx":1104,"fill":97,"fillOpacity":1147,"stroke":97,"style":1106},0.12,[99,1149,1151],{"x":1150,"y":1110,"style":1111,"fill":140,"textAnchor":105},"484","a long script",[99,1153,1116],{"x":1150,"y":1115,"style":1111,"fill":140,"textAnchor":105},[85,1155],{"x":1156,"y":1101,"width":1102,"height":1103,"rx":1104,"fill":119,"stroke":1121,"style":1122},"592",[99,1158,1160],{"x":1159,"y":1102,"style":1111,"fill":140,"textAnchor":105},"652","no external file",[222,1162],{"x1":1163,"y1":123,"x2":1164,"y2":123,"stroke":1089,"style":1131},"548","588",[99,1166,1167],{"x":1031,"y":1134,"style":1135,"fill":97,"textAnchor":105},"fully reproducible from source",[99,1169,1170],{"x":1031,"y":997,"style":1135,"fill":140,"textAnchor":105},"every design change is a code change",[270,1172,1174],{"id":1173},"qgis-version-compatibility-notes","QGIS-version compatibility notes",[14,1176,1177],{},"Standalone launches are far more sensitive to version drift than in-app scripting, because you are hard-coding paths and ABIs the application normally discovers for you.",[1179,1180,1181,1197],"table",{},[1182,1183,1184],"thead",{},[1185,1186,1187,1191,1194],"tr",{},[1188,1189,1190],"th",{},"Concern",[1188,1192,1193],{},"Requirement",[1188,1195,1196],{},"Failure symptom",[1198,1199,1200,1218,1235,1250,1272,1286],"tbody",{},[1185,1201,1202,1206,1209],{},[1203,1204,1205],"td",{},"Python minor version",[1203,1207,1208],{},"Match the QGIS-bundled version exactly (3.12 for 3.34+\u002F3.44 LTR; 3.9 for 3.28 LTR)",[1203,1210,1211,1214,1215],{},[38,1212,1213],{},"ImportError: DLL load failed"," or ",[38,1216,1217],{},"ModuleNotFoundError: qgis",[1185,1219,1220,1223,1230],{},[1203,1221,1222],{},"QGIS major.minor",[1203,1224,1225,1226,1229],{},"Target the installed release; newer classes raise ",[38,1227,1228],{},"AttributeError"," on older builds",[1203,1231,1232,1234],{},[38,1233,1228],{}," on a missing class or method",[1185,1236,1237,1240,1243],{},[1203,1238,1239],{},"Architecture",[1203,1241,1242],{},"64-bit interpreter against 64-bit QGIS",[1203,1244,1245,1246,1249],{},"Crash during ",[38,1247,1248],{},"QgsApplication"," init, not at import",[1185,1251,1252,1255,1269],{},[1203,1253,1254],{},"macOS prefix",[1203,1256,1257,1258,1260,1261,1264,1265,1268],{},"Point ",[38,1259,44],{}," at ",[38,1262,1263],{},"Contents\u002FMacOS",", not the ",[38,1266,1267],{},".app"," root",[1203,1270,1271],{},"App initializes but loads no providers",[1185,1273,1274,1277,1282],{},[1203,1275,1276],{},"Linux headless",[1203,1278,1279,1280],{},"Export ",[38,1281,954],{},[1203,1283,1284],{},[38,1285,958],{},[1185,1287,1288,1291,1301],{},[1203,1289,1290],{},"Path handling",[1203,1292,1293,1294,45,1297,1300],{},"Use ",[38,1295,1296],{},"os.path.join",[38,1298,1299],{},"os.pathsep",", never hard-coded separators",[1203,1302,1303,1306,1307],{},[38,1304,1305],{},"FileNotFoundError"," during ",[38,1308,1309],{},"setPrefixPath()",[14,1311,1312],{},"Pin your script to the LTR you actually deploy against. Patch releases within a minor version (3.12.1 vs 3.12.4) are interchangeable; minor versions (3.11 vs 3.12) are not.",[270,1314,1316],{"id":1315},"troubleshooting","Troubleshooting",[14,1318,1319],{},"If initialization fails or the process exits with no output, work through these in order.",[1321,1322,1323,1343,1385,1449,1468],"ol",{},[281,1324,1325,1328,1329,45,1331,1334,1335,1337,1338,1342],{},[284,1326,1327],{},"Verify path resolution first."," Print ",[38,1330,318],{},[38,1332,1333],{},"os.environ[\"QGIS_PREFIX_PATH\"]"," immediately after assignment. A mismatched interpreter — the terminal working while the scheduler or IDE runs a different Python — is the single most common cause of a missing ",[38,1336,40],{}," module. The parent ",[20,1339,1341],{"href":1340},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002F","Debugging PyQGIS Scripts"," guide covers isolating this cleanly.",[281,1344,1345,1352,1353,1384],{},[284,1346,1347,1348,1351],{},"Prefer ",[38,1349,1350],{},"qgis_process"," for CI\u002FCD."," QGIS 3.14+ ships a standalone CLI that auto-configures the environment, so you avoid manual path mapping entirely for algorithm runs:\n",[348,1354,1356],{"className":877,"code":1355,"language":879,"meta":353,"style":353},"qgis_process run native:buffer -- INPUT=roads.shp DISTANCE=100 OUTPUT=buffered.gpkg\n",[38,1357,1358],{"__ignoreMap":353},[357,1359,1360,1363,1366,1369,1372,1375,1378,1381],{"class":222,"line":359},[357,1361,1350],{"class":1362},"svObZ",[357,1364,1365],{"class":405}," run",[357,1367,1368],{"class":405}," native:buffer",[357,1370,1371],{"class":395}," --",[357,1373,1374],{"class":405}," INPUT=roads.shp",[357,1376,1377],{"class":405}," DISTANCE=",[357,1379,1380],{"class":395},"100",[357,1382,1383],{"class":405}," OUTPUT=buffered.gpkg\n","\nTo run custom Python this way, wrap your logic as a Processing algorithm. This is the most reliable fallback in a pipeline.",[281,1386,1387,1390,1391,1393,1394,1397,1398],{},[284,1388,1389],{},"Fall back to a Conda environment."," When system paths are unstable or ",[38,1392,48],{}," cannot be modified, ",[38,1395,1396],{},"conda-forge"," resolves GDAL, PROJ, Qt, and the SIP bindings for you:\n",[348,1399,1401],{"className":877,"code":1400,"language":879,"meta":353,"style":353},"conda create -n qgis-standalone -c conda-forge qgis python=3.12\nconda activate qgis-standalone\npython standalone_script.py\n",[38,1402,1403,1432,1442],{"__ignoreMap":353},[357,1404,1405,1408,1411,1414,1417,1420,1423,1426,1429],{"class":222,"line":359},[357,1406,1407],{"class":1362},"conda",[357,1409,1410],{"class":405}," create",[357,1412,1413],{"class":395}," -n",[357,1415,1416],{"class":405}," qgis-standalone",[357,1418,1419],{"class":395}," -c",[357,1421,1422],{"class":405}," conda-forge",[357,1424,1425],{"class":405}," qgis",[357,1427,1428],{"class":405}," python=",[357,1430,1431],{"class":395},"3.12\n",[357,1433,1434,1436,1439],{"class":222,"line":370},[357,1435,1407],{"class":1362},[357,1437,1438],{"class":405}," activate",[357,1440,1441],{"class":405}," qgis-standalone\n",[357,1443,1444,1446],{"class":222,"line":378},[357,1445,352],{"class":1362},[357,1447,1448],{"class":405}," standalone_script.py\n",[281,1450,1451,1454,1455,1457,1458,1460,1461,1464,1465,303],{},[284,1452,1453],{},"Surface silent provider failures."," If a layer loads with zero features but no error appears, GDAL\u002FOGR drivers were never registered — usually an incomplete ",[38,1456,44],{}," or a missed ",[38,1459,800],{},". Add ",[38,1462,1463],{},"from osgeo import gdal; gdal.UseExceptions()"," so driver errors are raised instead of swallowed, and log with ",[38,1466,1467],{},"QgsApplication.messageLog().logMessage(\"init ok\", \"Standalone\")",[281,1469,1470,1476,1477,1479,1480,45,1483,1486],{},[284,1471,1472,1473,1475],{},"Remove the PyPI ",[38,1474,40],{}," stub."," The ",[38,1478,40],{}," package on PyPI is a documentation placeholder that shadows the real bindings. Run ",[38,1481,1482],{},"pip list | grep qgis",[38,1484,1485],{},"pip uninstall qgis"," if it appears.",[270,1488,1490],{"id":1489},"conclusion","Conclusion",[14,1492,1493,1494,1496,1497,22,1499,1501,1502,1504,1505,1507,1508,1510,1511,1513,1514,1516,1517,1519,1520,1522,1523,303],{},"Running PyQGIS outside QGIS desktop comes down to three moves: extend ",[38,1495,52],{}," and set ",[38,1498,44],{},[38,1500,48],{}," before any ",[38,1503,40],{}," import, construct ",[38,1506,194],{}," and call ",[38,1509,800],{},", then register the Processing provider only if you need ",[38,1512,808],{}," algorithms. Match your Python minor version to the QGIS build exactly, export ",[38,1515,954],{}," on headless Linux, and clean up with ",[38,1518,220],{},". When manual path mapping stays fragile — locked-down runners, tangled schedulers — reach for ",[38,1521,1350],{}," or a Conda environment instead of fighting ",[38,1524,48],{},[270,1526,1528],{"id":1527},"frequently-asked-questions","Frequently Asked Questions",[14,1530,1531,1537,1538,1540,1541,45,1543,1545,1546,1548],{},[284,1532,1533,1534,1536],{},"Why must I set the environment variables before importing any ",[38,1535,40],{}," module?","\nImporting ",[38,1539,968],{}," triggers loading of compiled C++ libraries (Qt, GDAL, PROJ) that are resolved against ",[38,1542,48],{},[38,1544,44],{}," at import time. If you set those variables after the import, the dynamic linker has already searched the wrong directories, producing ",[38,1547,1213],{}," or silent provider failures.",[14,1550,1551,1560,1561,1563],{},[284,1552,1553,1554,1556,1557,1559],{},"What does the ",[38,1555,645],{}," argument in ",[38,1558,194],{}," do?","\nThe second argument controls GUI initialization. Passing ",[38,1562,645],{}," runs QGIS in headless mode, so no windows or display server are needed — that is what makes the script suitable for cron jobs, servers, and CI\u002FCD runners.",[14,1565,1566,1569,1570,1572],{},[284,1567,1568],{},"Why does my standalone script crash on a Linux server with no display?","\nEven in headless mode, Qt may try to connect to an X display. Export ",[38,1571,954],{}," before running so Qt uses its offscreen backend instead of attempting an X11 connection.",[14,1574,1575,1578,1579,1581,1582,1584,1585,1587,1588,1591],{},[284,1576,1577],{},"My layer loads with zero features but no error appears — what went wrong?","\nGDAL\u002FOGR drivers were not registered, usually because ",[38,1580,44],{}," is incomplete or ",[38,1583,800],{}," never ran. Verify the prefix path, confirm ",[38,1586,800],{}," executed, and call ",[38,1589,1590],{},"gdal.UseExceptions()"," so driver errors surface instead of failing silently.",[14,1593,1594,1603,1604,1606,1607,1609],{},[284,1595,1596,1597,1599,1600,1602],{},"When should I use ",[38,1598,1350],{}," instead of bootstrapping ",[38,1601,1248],{}," myself?","\nUse ",[38,1605,1350],{}," for CI\u002FCD and scheduled tasks that mainly run existing Processing algorithms — it auto-configures the environment and avoids fragile path mapping. Bootstrap ",[38,1608,1248],{}," directly only when you need custom Python logic beyond a single algorithm call.",[270,1611,1613],{"id":1612},"related","Related",[278,1615,1616,1621,1629,1637],{},[281,1617,1618,1620],{},[20,1619,32],{"href":31}," — parent guide to isolating a standalone interpreter",[281,1622,1623,1625,1626,1628],{},[20,1624,67],{"href":66}," — diagnose a broken ",[38,1627,149],{}," in the same bootstrap",[281,1630,1631,1633,1634,1636],{},[20,1632,866],{"href":865}," — drive ",[38,1635,804],{}," once the environment is up",[281,1638,1639],{},[20,1640,1642],{"href":1641},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Fhow-to-install-qgis-python-bindings-on-windows\u002F","How to Install QGIS Python Bindings on Windows",[1644,1645,1646],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}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 .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .sRjNt, html code.shiki .sRjNt{--shiki-default:#85E89D;--shiki-default-font-weight:bold}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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":353,"searchDepth":370,"depth":370,"links":1648},[1649,1650,1654,1655,1656,1657,1658,1659,1660],{"id":272,"depth":370,"text":273},{"id":330,"depth":370,"text":331,"children":1651},[1652,1653],{"id":790,"depth":378,"text":791},{"id":870,"depth":378,"text":871},{"id":961,"depth":370,"text":962},{"id":1053,"depth":370,"text":1054},{"id":1173,"depth":370,"text":1174},{"id":1315,"depth":370,"text":1316},{"id":1489,"depth":370,"text":1490},{"id":1527,"depth":370,"text":1528},{"id":1612,"depth":370,"text":1613},"Run PyQGIS scripts outside QGIS desktop by initializing QgsApplication, setting the prefix path, and loading data providers in a standalone, headless interpreter.","md",{"slug":12,"type":1664,"breadcrumb":1665,"datePublished":1666,"dateModified":1667},"article","Running Scripts Outside QGIS","2025-02-18","2026-07-18","\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop",{"title":5,"description":1661},"pyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002Findex","vFz-KP1GYVHUoK9r4BiZ2axXirtXZPQmCwnFpxF18ss",1787823360566]