[{"data":1,"prerenderedAt":2178},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects":3},{"id":4,"title":5,"body":6,"description":2167,"extension":2168,"meta":2169,"navigation":242,"path":2174,"seo":2175,"stem":2176,"__hash__":2177},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Findex.md","Working with QGIS Projects in PyQGIS",{"type":7,"value":8,"toc":2154},"minimark",[9,13,17,36,195,200,210,309,332,343,347,350,425,458,476,484,600,604,607,626,726,746,758,872,876,879,931,958,969,976,1036,1044,1048,1051,1144,1165,1172,1222,1238,1333,1337,1340,1507,1515,1528,1532,1535,1586,1607,1610,1646,1690,1695,1787,1791,1794,1797,1809,1819,1825,1948,1969,1972,1976,2024,2028,2037,2053,2072,2085,2096,2106,2110,2150],[10,11,5],"h1",{"id":12},"working-with-qgis-projects-in-pyqgis",[14,15,16],"p",{},"Almost every PyQGIS example starts with a layer, which quietly skips the thing that holds the layers together. A QGIS project is the document your users actually open: it remembers which datasets are involved, how they are styled, what order they draw in, which coordinate system the map is displayed in, which layouts exist, and a hundred small preferences that nobody wants to set twice. Automating QGIS without touching projects means rebuilding all of that in code every time a script runs.",[14,18,19,20,25,26,30,31,35],{},"This guide sits inside ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002F","PyQGIS Fundamentals & Environment Setup"," and covers the ",[27,28,29],"code",{},"QgsProject"," API from both directions: reading an existing project so a script can work with what a cartographer already built, and writing one so the output of an automated pipeline is something a human can open. It is the piece that makes the unattended workflows in ",[21,32,34],{"href":33},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","Headless QGIS and Server Automation"," practical, because the fastest way to produce a good map from a script is to start from a project somebody designed by hand.",[14,37,38],{},[39,40,45,49,53,60,77,86,95,101,110,115,120,124,128,132,137,141,148,152,155,158,161,165,169,173,180,186,191],"svg",{"viewBox":41,"role":42,"ariaLabel":43,"xmlns":44},"0 0 760 300","img","Anatomy of a QGIS project file showing the zipped container holding an XML document that stores layer data source references, styling, layer tree order, layouts and project properties, with the actual spatial data living outside the file","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[46,47,48],"title",{},"What is actually inside a QGIS project",[50,51,52],"desc",{},"A .qgz file is a zip container holding a .qgs XML document and an optional auxiliary storage database. The XML records data source references, styling, the layer tree order, print layouts, the project coordinate system and project variables. The spatial data itself stays outside the project, in files or a database, and is only referenced by path or connection string.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59},"0","760","300","#f6f3ea",[61,62,63],"defs",{},[64,65,72],"marker",{"id":66,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"projAnatArrow","0 0 10 10","8","5","7","auto-start-reverse",[73,74],"path",{"d":75,"fill":76},"M0 0 L10 5 L0 10 z","#2f3b35",[78,79,85],"text",{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","A project stores references and decisions, never the data",[54,87],{"x":88,"y":89,"width":58,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"20","48","216","10","#eef7f4","#0f766e","stroke-width:2.5",[78,96,100],{"x":97,"y":98,"style":99,"fill":93,"textAnchor":84},"170","74","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","project.qgz — a zip container",[54,102],{"x":103,"y":104,"width":105,"height":106,"rx":68,"fill":107,"stroke":108,"style":109},"38","88","264","120","#fffdf7","#59645f","stroke-width:1.5",[78,111,114],{"x":97,"y":112,"style":113,"fill":83,"textAnchor":84},"110","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","project.qgs — XML",[78,116,119],{"x":97,"y":117,"style":118,"fill":76,"textAnchor":84},"130","text-anchor:middle;font-size:11px;font-family:sans-serif","data source URI and provider per layer",[78,121,123],{"x":97,"y":122,"style":118,"fill":76,"textAnchor":84},"148","symbology, labelling, field aliases",[78,125,127],{"x":97,"y":126,"style":118,"fill":76,"textAnchor":84},"166","layer tree order, groups, visibility",[78,129,131],{"x":97,"y":130,"style":118,"fill":76,"textAnchor":84},"184","layouts, project CRS, variables",[54,133],{"x":103,"y":90,"width":105,"height":134,"rx":68,"fill":135,"stroke":136,"style":109},"34","#fdf2e2","#b45309",[78,138,140],{"x":97,"y":139,"style":118,"fill":76,"textAnchor":84},"238","project.qgd — auxiliary storage, if used",[54,142],{"x":143,"y":144,"width":145,"height":104,"rx":91,"fill":146,"stroke":147,"style":94},"452","60","288","#eff3ff","#2563eb",[78,149,151],{"x":150,"y":104,"style":99,"fill":147,"textAnchor":84},"596","the data lives here",[78,153,154],{"x":150,"y":112,"style":118,"fill":76,"textAnchor":84},"roads.gpkg, dem.tif, a PostGIS table",[78,156,157],{"x":150,"y":117,"style":118,"fill":76,"textAnchor":84},"a WFS endpoint, an XYZ tile service",[54,159],{"x":143,"y":160,"width":145,"height":104,"rx":91,"fill":135,"stroke":136,"style":94},"176",[78,162,164],{"x":150,"y":163,"style":99,"fill":136,"textAnchor":84},"204","so moving data breaks the project",[78,166,168],{"x":150,"y":167,"style":118,"fill":76,"textAnchor":84},"226","relative paths survive moving the folder",[78,170,172],{"x":150,"y":171,"style":118,"fill":76,"textAnchor":84},"246","absolute paths survive nothing else",[174,175],"line",{"x1":176,"y1":106,"x2":177,"y2":178,"stroke":76,"style":179},"320","446","104","stroke-width:2;marker-end:url(#projAnatArrow)",[78,181,185],{"x":182,"y":183,"style":184,"fill":108,"textAnchor":84},"384","100","text-anchor:middle;font-size:10px;font-family:sans-serif","refers to",[174,187],{"x1":176,"y1":188,"x2":177,"y2":189,"stroke":136,"style":190},"200","214","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#projAnatArrow)",[78,192,194],{"x":80,"y":193,"style":118,"fill":108,"textAnchor":84},"286","A 40 KB project can describe 40 GB of data — and can be regenerated in seconds",[196,197,199],"h2",{"id":198},"the-project-object-every-script-already-has","The project object every script already has",[14,201,202,205,206,209],{},[27,203,204],{},"QgsProject.instance()"," returns the single project the running QGIS is working with. In the Python console that is whatever the user has open; in a standalone script it is an empty project created for you when ",[27,207,208],{},"QgsApplication"," initialises. Everything on it is available immediately.",[211,212,217],"pre",{"className":213,"code":214,"language":215,"meta":216,"style":216},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nproject = QgsProject.instance()\nprint(project.fileName())                 # '' until it has been saved or read\nprint(project.crs().authid())             # 'EPSG:4326' on a fresh project\nprint(len(project.mapLayers()))           # layers currently registered\nprint(project.isDirty())                  # unsaved changes present?\n","python","",[27,218,219,237,244,256,270,281,298],{"__ignoreMap":216},[220,221,223,227,231,234],"span",{"class":174,"line":222},1,[220,224,226],{"class":225},"snl16","from",[220,228,230],{"class":229},"s95oV"," qgis.core ",[220,232,233],{"class":225},"import",[220,235,236],{"class":229}," QgsProject\n",[220,238,240],{"class":174,"line":239},2,[220,241,243],{"emptyLinePlaceholder":242},true,"\n",[220,245,247,250,253],{"class":174,"line":246},3,[220,248,249],{"class":229},"project ",[220,251,252],{"class":225},"=",[220,254,255],{"class":229}," QgsProject.instance()\n",[220,257,259,263,266],{"class":174,"line":258},4,[220,260,262],{"class":261},"sDLfK","print",[220,264,265],{"class":229},"(project.fileName())                 ",[220,267,269],{"class":268},"sjoCn","# '' until it has been saved or read\n",[220,271,273,275,278],{"class":174,"line":272},5,[220,274,262],{"class":261},[220,276,277],{"class":229},"(project.crs().authid())             ",[220,279,280],{"class":268},"# 'EPSG:4326' on a fresh project\n",[220,282,284,286,289,292,295],{"class":174,"line":283},6,[220,285,262],{"class":261},[220,287,288],{"class":229},"(",[220,290,291],{"class":261},"len",[220,293,294],{"class":229},"(project.mapLayers()))           ",[220,296,297],{"class":268},"# layers currently registered\n",[220,299,301,303,306],{"class":174,"line":300},7,[220,302,262],{"class":261},[220,304,305],{"class":229},"(project.isDirty())                  ",[220,307,308],{"class":268},"# unsaved changes present?\n",[14,310,311,315,316,319,320,323,324,327,328,331],{},[312,313,314],"strong",{},"Breakdown:"," ",[27,317,318],{},"mapLayers()"," returns a dictionary keyed by layer id, not by name — ids are stable across renames, which is why the project stores them and why ",[27,321,322],{},"mapLayersByName()"," is a convenience rather than an identity mechanism. ",[27,325,326],{},"isDirty()"," is the flag QGIS uses to decide whether to prompt on close; a script that changes a project and then does not write it leaves that flag set, which is exactly why an automated job should either write explicitly or clear the flag before exiting. ",[27,329,330],{},"fileName()"," being empty is the reliable way to tell an unsaved project from a loaded one.",[14,333,334,335,338,339,342],{},"For a second project loaded alongside the current one — comparing two, or merging layers from one into another — construct ",[27,336,337],{},"QgsProject()"," directly instead of using the singleton. Nothing in the API forces you through ",[27,340,341],{},"instance()",", and keeping a batch job's working project separate from the user's open project avoids a whole class of surprise.",[196,344,346],{"id":345},"reading-and-writing","Reading and writing",[14,348,349],{},"Two methods do the file work, and both return a boolean you must check.",[211,351,353],{"className":213,"code":352,"language":215,"meta":216,"style":216},"project = QgsProject.instance()\n\nif not project.read(\"\u002Fdata\u002Fprojects\u002Fflood_atlas.qgz\"):\n    raise RuntimeError(\"project failed to load\")\n\nproject.setTitle(\"Flood atlas — August\")\nproject.write(\"\u002Fdata\u002Fprojects\u002Fflood_atlas_august.qgz\")\n",[27,354,355,363,367,385,401,405,415],{"__ignoreMap":216},[220,356,357,359,361],{"class":174,"line":222},[220,358,249],{"class":229},[220,360,252],{"class":225},[220,362,255],{"class":229},[220,364,365],{"class":174,"line":239},[220,366,243],{"emptyLinePlaceholder":242},[220,368,369,372,375,378,382],{"class":174,"line":246},[220,370,371],{"class":225},"if",[220,373,374],{"class":225}," not",[220,376,377],{"class":229}," project.read(",[220,379,381],{"class":380},"sU2Wk","\"\u002Fdata\u002Fprojects\u002Fflood_atlas.qgz\"",[220,383,384],{"class":229},"):\n",[220,386,387,390,393,395,398],{"class":174,"line":258},[220,388,389],{"class":225},"    raise",[220,391,392],{"class":261}," RuntimeError",[220,394,288],{"class":229},[220,396,397],{"class":380},"\"project failed to load\"",[220,399,400],{"class":229},")\n",[220,402,403],{"class":174,"line":272},[220,404,243],{"emptyLinePlaceholder":242},[220,406,407,410,413],{"class":174,"line":283},[220,408,409],{"class":229},"project.setTitle(",[220,411,412],{"class":380},"\"Flood atlas — August\"",[220,414,400],{"class":229},[220,416,417,420,423],{"class":174,"line":300},[220,418,419],{"class":229},"project.write(",[220,421,422],{"class":380},"\"\u002Fdata\u002Fprojects\u002Fflood_atlas_august.qgz\"",[220,424,400],{"class":229},[14,426,427,315,429,432,433,436,437,440,441,443,444,446,447,450,451,454,455,457],{},[312,428,314],{},[27,430,431],{},"read()"," replaces the entire contents of the project object, so anything already loaded is discarded — call ",[27,434,435],{},"project.clear()"," first if you want that to be explicit rather than implicit. Passing a path to ",[27,438,439],{},"write()"," performs a save-as and updates ",[27,442,330],{},"; calling ",[27,445,439],{}," with no arguments saves back over the file that was read, which is the form to avoid in a scheduled job unless overwriting the source is genuinely intended. Both ",[27,448,449],{},".qgs"," (plain XML) and ",[27,452,453],{},".qgz"," (zipped) are chosen by the extension you supply; ",[27,456,453],{}," is smaller and keeps auxiliary storage in the same file, and is the better default for anything that gets emailed or committed.",[14,459,460,461,464,465,464,468,471,472,475],{},"A project read from disk fires signals as it loads — ",[27,462,463],{},"layerLoaded",", ",[27,466,467],{},"readProject",[27,469,470],{},"homePathChanged"," — which is how plugins hook into project opening. In a script the useful one is ",[27,473,474],{},"layerWasAdded",", because it lets you attach behaviour to every layer without knowing what the project contains.",[14,477,478,479,483],{},"The full walkthrough, including reading a project whose layers are unavailable and writing to a template folder, is in ",[21,480,482],{"href":481},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis\u002F","Save and Load a QGIS Project in PyQGIS",".",[14,485,486],{},[39,487,490,493,496,499,506,509,515,520,524,527,531,534,538,542,545,551,555,558,563,567,571,576,581,584,586,589,592,596],{"viewBox":488,"role":42,"ariaLabel":489,"xmlns":44},"0 0 760 244","Project lifecycle in a script running from clear through read, modify, and write, with the dirty flag shown as set by modification and cleared by a successful write",[46,491,492],{},"The lifecycle of a project inside a script",[50,494,495],{},"A left-to-right sequence: clear the project, read a file, modify layers and settings, then write. Underneath, a track shows the dirty flag as false after reading, true after any modification, and false again after a successful write. A branch notes that exiting while dirty simply discards the changes in a headless script, with no prompt.",[54,497],{"x":56,"y":56,"width":57,"height":498,"fill":59},"244",[61,500,501],{},[64,502,504],{"id":503,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"projLifeArrow",[73,505],{"d":75,"fill":76},[78,507,508],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"Nothing is written until you write it",[54,510],{"x":88,"y":511,"width":512,"height":513,"rx":68,"fill":107,"stroke":108,"style":514},"52","150","56","stroke-width:2",[78,516,519],{"x":517,"y":518,"style":113,"fill":83,"textAnchor":84},"95","76","clear()",[78,521,523],{"x":517,"y":522,"style":118,"fill":76,"textAnchor":84},"94","start from nothing",[54,525],{"x":526,"y":511,"width":512,"height":513,"rx":68,"fill":92,"stroke":93,"style":514},"196",[78,528,530],{"x":529,"y":518,"style":113,"fill":93,"textAnchor":84},"271","read(path)",[78,532,533],{"x":529,"y":522,"style":118,"fill":76,"textAnchor":84},"returns False on failure",[54,535],{"x":536,"y":511,"width":537,"height":513,"rx":68,"fill":146,"stroke":147,"style":514},"372","180",[78,539,541],{"x":540,"y":518,"style":113,"fill":147,"textAnchor":84},"462","modify",[78,543,544],{"x":540,"y":522,"style":118,"fill":76,"textAnchor":84},"layers, styles, variables",[54,546],{"x":547,"y":511,"width":548,"height":513,"rx":68,"fill":549,"stroke":550,"style":514},"578","162","#edf8e9","#15803d",[78,552,554],{"x":553,"y":518,"style":113,"fill":550,"textAnchor":84},"659","write(path)",[78,556,557],{"x":553,"y":522,"style":118,"fill":76,"textAnchor":84},"check the return value",[174,559],{"x1":97,"y1":560,"x2":561,"y2":560,"stroke":76,"style":562},"80","190","stroke-width:2;marker-end:url(#projLifeArrow)",[174,564],{"x1":565,"y1":560,"x2":566,"y2":560,"stroke":76,"style":562},"346","366",[174,568],{"x1":569,"y1":560,"x2":570,"y2":560,"stroke":76,"style":562},"552","572",[54,572],{"x":88,"y":573,"width":574,"height":575,"rx":68,"fill":107,"stroke":108,"style":109},"134","720","46",[78,577,326],{"x":578,"y":579,"style":580,"fill":108},"40","152","font-size:10px;font-family:sans-serif",[78,582,583],{"x":517,"y":97,"style":118,"fill":76,"textAnchor":84},"false",[78,585,583],{"x":529,"y":97,"style":118,"fill":76,"textAnchor":84},[78,587,588],{"x":540,"y":97,"style":113,"fill":136,"textAnchor":84},"true",[78,590,591],{"x":553,"y":97,"style":118,"fill":550,"textAnchor":84},"false again",[54,593],{"x":526,"y":594,"width":595,"height":134,"rx":68,"fill":135,"stroke":136,"style":514},"194","544",[78,597,599],{"x":598,"y":90,"style":118,"fill":76,"textAnchor":84},"468","A headless script that exits while dirty simply loses the work — there is nobody to prompt",[196,601,603],{"id":602},"two-lists-not-one-the-registry-and-the-layer-tree","Two lists, not one: the registry and the layer tree",[14,605,606],{},"The single most useful thing to understand about projects is that a layer exists in two places, and confusing them produces the classic \"my layer loaded but nothing appears in the panel\" bug.",[14,608,609,610,613,614,617,618,621,622,625],{},"The ",[312,611,612],{},"registry"," is ownership: ",[27,615,616],{},"QgsProject.addMapLayer()"," puts a layer into the project so it stays alive, gets saved, and can be found by id. The ",[312,619,620],{},"layer tree"," is presentation: the ordered, groupable structure the Layers panel draws and the map canvas renders from. By default ",[27,623,624],{},"addMapLayer()"," does both, and the second argument is what separates them.",[211,627,629],{"className":213,"code":628,"language":215,"meta":216,"style":216},"from qgis.core import QgsProject, QgsVectorLayer\n\nproject = QgsProject.instance()\nlayer = QgsVectorLayer(\"\u002Fdata\u002Froads.gpkg|layername=roads\", \"Roads\", \"ogr\")\n\nproject.addMapLayer(layer, False)              # register, but do not show\ngroup = project.layerTreeRoot().findGroup(\"Base map\")\ngroup.insertLayer(0, layer)                    # decide exactly where it appears\n",[27,630,631,642,646,654,679,683,697,712],{"__ignoreMap":216},[220,632,633,635,637,639],{"class":174,"line":222},[220,634,226],{"class":225},[220,636,230],{"class":229},[220,638,233],{"class":225},[220,640,641],{"class":229}," QgsProject, QgsVectorLayer\n",[220,643,644],{"class":174,"line":239},[220,645,243],{"emptyLinePlaceholder":242},[220,647,648,650,652],{"class":174,"line":246},[220,649,249],{"class":229},[220,651,252],{"class":225},[220,653,255],{"class":229},[220,655,656,659,661,664,667,669,672,674,677],{"class":174,"line":258},[220,657,658],{"class":229},"layer ",[220,660,252],{"class":225},[220,662,663],{"class":229}," QgsVectorLayer(",[220,665,666],{"class":380},"\"\u002Fdata\u002Froads.gpkg|layername=roads\"",[220,668,464],{"class":229},[220,670,671],{"class":380},"\"Roads\"",[220,673,464],{"class":229},[220,675,676],{"class":380},"\"ogr\"",[220,678,400],{"class":229},[220,680,681],{"class":174,"line":272},[220,682,243],{"emptyLinePlaceholder":242},[220,684,685,688,691,694],{"class":174,"line":283},[220,686,687],{"class":229},"project.addMapLayer(layer, ",[220,689,690],{"class":261},"False",[220,692,693],{"class":229},")              ",[220,695,696],{"class":268},"# register, but do not show\n",[220,698,699,702,704,707,710],{"class":174,"line":300},[220,700,701],{"class":229},"group ",[220,703,252],{"class":225},[220,705,706],{"class":229}," project.layerTreeRoot().findGroup(",[220,708,709],{"class":380},"\"Base map\"",[220,711,400],{"class":229},[220,713,715,718,720,723],{"class":174,"line":714},8,[220,716,717],{"class":229},"group.insertLayer(",[220,719,56],{"class":261},[220,721,722],{"class":229},", layer)                    ",[220,724,725],{"class":268},"# decide exactly where it appears\n",[14,727,728,730,731,733,734,737,738,741,742,745],{},[312,729,314],{}," Passing ",[27,732,690],{}," as ",[27,735,736],{},"addToLegend"," registers the layer without touching the tree, and the following two lines then place it precisely — inside a named group, at the top. Doing it in one step with ",[27,739,740],{},"addMapLayer(layer)"," always appends at the top level, which is why scripts that build a structured project use the two-step form. The reverse operation follows the same split: ",[27,743,744],{},"removeMapLayer(layer.id())"," deletes it from both, while removing only the tree node leaves an orphaned registered layer that still gets written to the file.",[14,747,748,749,753,754,483],{},"Groups, visibility, ordering and check states all live on the tree, and ",[21,750,752],{"href":751},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Forganise-layer-tree-groups-pyqgis\u002F","Organise the Layer Tree with Groups in PyQGIS"," works through the node API. Adding and removing layers safely — including the signal-ordering traps when other code is listening — is covered in ",[21,755,757],{"href":756},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fadd-and-remove-layers-from-project-pyqgis\u002F","Add and Remove Layers from a Project in PyQGIS",[14,759,760],{},[39,761,764,767,770,773,780,783,786,790,796,800,803,807,809,813,817,820,824,828,833,838,843,845,849,852,855,861,864,868],{"viewBox":762,"role":42,"ariaLabel":763,"xmlns":44},"0 0 760 280","Diagram separating the project layer registry which owns layers by identifier from the layer tree which orders and groups them for display, with one layer registered but not shown and another shown inside a group",[46,765,766],{},"The registry owns layers; the tree arranges them",[50,768,769],{},"On the left, the project registry lists three layers by identifier with no order or grouping. On the right, the layer tree shows a group containing two of them in a chosen order, while the third is registered but absent from the tree and therefore invisible in the Layers panel even though it will still be saved with the project.",[54,771],{"x":56,"y":56,"width":57,"height":772,"fill":59},"280",[61,774,775],{},[64,776,778],{"id":777,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"projTreeArrow",[73,779],{"d":75,"fill":108},[78,781,782],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"Registered is not the same as visible",[54,784],{"x":785,"y":89,"width":58,"height":163,"rx":91,"fill":146,"stroke":147,"style":94},"24",[78,787,789],{"x":788,"y":98,"style":99,"fill":147,"textAnchor":84},"174","registry — project.mapLayers()",[54,791],{"x":792,"y":104,"width":793,"height":794,"rx":795,"fill":107,"stroke":108,"style":109},"44","260","36","6",[78,797,799],{"x":788,"y":798,"style":118,"fill":76,"textAnchor":84},"111","roads_9f2c1 — Roads",[54,801],{"x":792,"y":802,"width":793,"height":794,"rx":795,"fill":107,"stroke":108,"style":109},"132",[78,804,806],{"x":788,"y":805,"style":118,"fill":76,"textAnchor":84},"155","parcels_44ab7 — Parcels",[54,808],{"x":792,"y":160,"width":793,"height":794,"rx":795,"fill":135,"stroke":136,"style":109},[78,810,812],{"x":788,"y":811,"style":118,"fill":76,"textAnchor":84},"199","scratch_10df3 — Scratch",[78,814,816],{"x":788,"y":815,"style":184,"fill":108,"textAnchor":84},"232","unordered, keyed by layer id, all saved",[54,818],{"x":819,"y":89,"width":58,"height":163,"rx":91,"fill":92,"stroke":93,"style":94},"436",[78,821,823],{"x":822,"y":98,"style":99,"fill":93,"textAnchor":84},"586","tree — project.layerTreeRoot()",[54,825],{"x":826,"y":104,"width":793,"height":827,"rx":795,"fill":107,"stroke":93,"style":109},"456","96",[78,829,832],{"x":830,"y":112,"style":831,"fill":83},"472","font-size:11px;font-weight:bold;font-family:sans-serif","group: Base map",[54,834],{"x":835,"y":106,"width":90,"height":836,"rx":69,"fill":92,"stroke":108,"style":837},"484","26","stroke-width:1.2",[78,839,842],{"x":840,"y":841,"style":118,"fill":76,"textAnchor":84},"592","138","Roads — visible",[54,844],{"x":835,"y":512,"width":90,"height":836,"rx":69,"fill":92,"stroke":108,"style":837},[78,846,848],{"x":840,"y":847,"style":118,"fill":76,"textAnchor":84},"168","Parcels — unchecked",[54,850],{"x":826,"y":594,"width":793,"height":134,"rx":795,"fill":59,"stroke":136,"style":851},"stroke-width:1.5;stroke-dasharray:5 4",[78,853,854],{"x":822,"y":90,"style":118,"fill":136,"textAnchor":84},"Scratch is not here — and not drawn",[174,856],{"x1":857,"y1":858,"x2":859,"y2":117,"stroke":108,"style":860},"324","106","450","stroke-width:2;marker-end:url(#projTreeArrow)",[174,862],{"x1":857,"y1":512,"x2":859,"y2":863,"stroke":108,"style":860},"160",[174,865],{"x1":857,"y1":594,"x2":859,"y2":866,"stroke":136,"style":867},"208","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#projTreeArrow)",[78,869,871],{"x":80,"y":870,"style":118,"fill":108,"textAnchor":84},"270","addMapLayer(layer, False) registers without showing — the second argument is the whole difference",[196,873,875],{"id":874},"paths-and-why-projects-break-when-they-move","Paths, and why projects break when they move",[14,877,878],{},"A project stores a data source string per layer, and whether that string is absolute or relative is a project property rather than a per-layer one. Get it wrong and the project works perfectly on the machine that made it and nowhere else.",[211,880,882],{"className":213,"code":881,"language":215,"meta":216,"style":216},"project.writeEntryBool(\"Paths\", \"\u002FAbsolute\", False)   # store relative paths\nprint(project.homePath())                             # the folder paths resolve against\nproject.setPresetHomePath(\"\u002Fdata\u002Fprojects\u002Fflood\")     # override it explicitly\n",[27,883,884,907,917],{"__ignoreMap":216},[220,885,886,889,892,894,897,899,901,904],{"class":174,"line":222},[220,887,888],{"class":229},"project.writeEntryBool(",[220,890,891],{"class":380},"\"Paths\"",[220,893,464],{"class":229},[220,895,896],{"class":380},"\"\u002FAbsolute\"",[220,898,464],{"class":229},[220,900,690],{"class":261},[220,902,903],{"class":229},")   ",[220,905,906],{"class":268},"# store relative paths\n",[220,908,909,911,914],{"class":174,"line":239},[220,910,262],{"class":261},[220,912,913],{"class":229},"(project.homePath())                             ",[220,915,916],{"class":268},"# the folder paths resolve against\n",[220,918,919,922,925,928],{"class":174,"line":246},[220,920,921],{"class":229},"project.setPresetHomePath(",[220,923,924],{"class":380},"\"\u002Fdata\u002Fprojects\u002Fflood\"",[220,926,927],{"class":229},")     ",[220,929,930],{"class":268},"# override it explicitly\n",[14,932,933,315,935,938,939,942,943,946,947,949,950,953,954,957],{},[312,934,314],{},[27,936,937],{},"writeEntry*"," and ",[27,940,941],{},"readEntry*"," are the generic key–value store every part of QGIS uses for project settings, addressed by a scope and a key — the same mechanism plugins use to persist their own per-project state. Setting ",[27,944,945],{},"Paths\u002FAbsolute"," to ",[27,948,690],{}," makes new saves record data sources relative to the project folder, which is what you want for anything that travels: a folder containing the project and its data can be copied, zipped or checked into version control and still opens. ",[27,951,952],{},"homePath()"," returns the project's folder unless a preset home path overrides it, and it is also what the ",[27,955,956],{},"@project_home"," expression variable resolves to.",[14,959,960,961,938,965,483],{},"Relative paths only help when the data actually travels with the project. For layers that live in a database or a web service the question does not arise: the connection string is location-independent already, which is one more argument for the workflows in ",[21,962,964],{"href":963},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002F","PostGIS and Database Workflows in PyQGIS",[21,966,968],{"href":967},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002F","Web Services and Remote Data in PyQGIS",[14,970,971,972,975],{},"When a project does open with broken layers, they are not lost — QGIS keeps the invalid layers with their original source strings, and ",[27,973,974],{},"layer.dataProvider().isValid()"," tells you which. Rewriting the source in place is the repair:",[211,977,979],{"className":213,"code":978,"language":215,"meta":216,"style":216},"for layer in project.mapLayers().values():\n    if not layer.isValid():\n        old = layer.source()\n        layer.setDataSource(old.replace(\"\u002Fmnt\u002Fold_share\u002F\", \"\u002Fdata\u002F\"),\n                            layer.name(), layer.providerType())\n",[27,980,981,995,1005,1015,1031],{"__ignoreMap":216},[220,982,983,986,989,992],{"class":174,"line":222},[220,984,985],{"class":225},"for",[220,987,988],{"class":229}," layer ",[220,990,991],{"class":225},"in",[220,993,994],{"class":229}," project.mapLayers().values():\n",[220,996,997,1000,1002],{"class":174,"line":239},[220,998,999],{"class":225},"    if",[220,1001,374],{"class":225},[220,1003,1004],{"class":229}," layer.isValid():\n",[220,1006,1007,1010,1012],{"class":174,"line":246},[220,1008,1009],{"class":229},"        old ",[220,1011,252],{"class":225},[220,1013,1014],{"class":229}," layer.source()\n",[220,1016,1017,1020,1023,1025,1028],{"class":174,"line":258},[220,1018,1019],{"class":229},"        layer.setDataSource(old.replace(",[220,1021,1022],{"class":380},"\"\u002Fmnt\u002Fold_share\u002F\"",[220,1024,464],{"class":229},[220,1026,1027],{"class":380},"\"\u002Fdata\u002F\"",[220,1029,1030],{"class":229},"),\n",[220,1032,1033],{"class":174,"line":272},[220,1034,1035],{"class":229},"                            layer.name(), layer.providerType())\n",[14,1037,1038,315,1040,1043],{},[312,1039,314],{},[27,1041,1042],{},"setDataSource()"," re-points an existing layer object, keeping its id, styling, labelling and every reference to it from layouts and joins — which is exactly what makes it the right tool and a string replacement on the XML the wrong one. The provider type must be passed unchanged unless you are genuinely switching provider, and the layer becomes valid immediately if the new source resolves.",[196,1045,1047],{"id":1046},"project-variables-and-metadata","Project variables and metadata",[14,1049,1050],{},"Variables are how a project carries values into expressions without hard-coding them in every label, layout and data-defined override. They are ordinary key–value pairs stored in the project file, readable and writable from Python.",[211,1052,1054],{"className":213,"code":1053,"language":215,"meta":216,"style":216},"from qgis.core import QgsExpressionContextUtils\n\nQgsExpressionContextUtils.setProjectVariable(project, \"survey_round\", \"2026-Q3\")\nQgsExpressionContextUtils.setProjectVariable(project, \"client_name\", \"Riverside Council\")\n\nvariables = QgsExpressionContextUtils.projectScope(project).variableNames()\nprint([name for name in variables if not name.startswith(\"qgis_\")])\n",[27,1055,1056,1067,1071,1086,1100,1104,1114],{"__ignoreMap":216},[220,1057,1058,1060,1062,1064],{"class":174,"line":222},[220,1059,226],{"class":225},[220,1061,230],{"class":229},[220,1063,233],{"class":225},[220,1065,1066],{"class":229}," QgsExpressionContextUtils\n",[220,1068,1069],{"class":174,"line":239},[220,1070,243],{"emptyLinePlaceholder":242},[220,1072,1073,1076,1079,1081,1084],{"class":174,"line":246},[220,1074,1075],{"class":229},"QgsExpressionContextUtils.setProjectVariable(project, ",[220,1077,1078],{"class":380},"\"survey_round\"",[220,1080,464],{"class":229},[220,1082,1083],{"class":380},"\"2026-Q3\"",[220,1085,400],{"class":229},[220,1087,1088,1090,1093,1095,1098],{"class":174,"line":258},[220,1089,1075],{"class":229},[220,1091,1092],{"class":380},"\"client_name\"",[220,1094,464],{"class":229},[220,1096,1097],{"class":380},"\"Riverside Council\"",[220,1099,400],{"class":229},[220,1101,1102],{"class":174,"line":272},[220,1103,243],{"emptyLinePlaceholder":242},[220,1105,1106,1109,1111],{"class":174,"line":283},[220,1107,1108],{"class":229},"variables ",[220,1110,252],{"class":225},[220,1112,1113],{"class":229}," QgsExpressionContextUtils.projectScope(project).variableNames()\n",[220,1115,1116,1118,1121,1123,1126,1128,1131,1133,1135,1138,1141],{"class":174,"line":300},[220,1117,262],{"class":261},[220,1119,1120],{"class":229},"([name ",[220,1122,985],{"class":225},[220,1124,1125],{"class":229}," name ",[220,1127,991],{"class":225},[220,1129,1130],{"class":229}," variables ",[220,1132,371],{"class":225},[220,1134,374],{"class":225},[220,1136,1137],{"class":229}," name.startswith(",[220,1139,1140],{"class":380},"\"qgis_\"",[220,1142,1143],{"class":229},")])\n",[14,1145,1146,1148,1149,1152,1153,1156,1157,1160,1161,1164],{},[312,1147,314],{}," A project variable set this way is available anywhere expressions are evaluated — as ",[27,1150,1151],{},"@survey_round"," in a label, a layout title, a filter or a data-defined size — so one Python assignment updates every place the value appears. Values are stored as strings in the project file even when set from a number, which is why comparisons in expressions often need ",[27,1154,1155],{},"to_int()"," or ",[27,1158,1159],{},"to_real()",". The scope object also exposes QGIS's own built-in variables, hence filtering the ",[27,1162,1163],{},"qgis_"," prefix when you only want your own.",[14,1166,1167,1168,1171],{},"Project ",[312,1169,1170],{},"metadata"," is the separate, structured record — title, abstract, author, contact, licence, keywords, extent — that matters when projects are catalogued or published to QGIS Server.",[211,1173,1175],{"className":213,"code":1174,"language":215,"meta":216,"style":216},"metadata = project.metadata()\nmetadata.setTitle(\"Flood risk atlas 2026\")\nmetadata.setAbstract(\"Modelled 1-in-100 year extents by ward, updated quarterly.\")\nmetadata.setLanguage(\"en-GB\")\nproject.setMetadata(metadata)\n",[27,1176,1177,1187,1197,1207,1217],{"__ignoreMap":216},[220,1178,1179,1182,1184],{"class":174,"line":222},[220,1180,1181],{"class":229},"metadata ",[220,1183,252],{"class":225},[220,1185,1186],{"class":229}," project.metadata()\n",[220,1188,1189,1192,1195],{"class":174,"line":239},[220,1190,1191],{"class":229},"metadata.setTitle(",[220,1193,1194],{"class":380},"\"Flood risk atlas 2026\"",[220,1196,400],{"class":229},[220,1198,1199,1202,1205],{"class":174,"line":246},[220,1200,1201],{"class":229},"metadata.setAbstract(",[220,1203,1204],{"class":380},"\"Modelled 1-in-100 year extents by ward, updated quarterly.\"",[220,1206,400],{"class":229},[220,1208,1209,1212,1215],{"class":174,"line":258},[220,1210,1211],{"class":229},"metadata.setLanguage(",[220,1213,1214],{"class":380},"\"en-GB\"",[220,1216,400],{"class":229},[220,1218,1219],{"class":174,"line":272},[220,1220,1221],{"class":229},"project.setMetadata(metadata)\n",[14,1223,1224,315,1226,1229,1230,1233,1234,483],{},[312,1225,314],{},[27,1227,1228],{},"metadata()"," returns a copy, so the object must be handed back with ",[27,1231,1232],{},"setMetadata()"," — modifying it in place changes nothing, which is a common and entirely silent mistake. Filling these fields costs four lines and makes a published project self-describing; both are worked through in ",[21,1235,1237],{"href":1236},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fuse-project-variables-and-metadata-pyqgis\u002F","Use Project Variables and Metadata in PyQGIS",[14,1239,1240],{},[39,1241,1244,1247,1250,1253,1260,1263,1267,1271,1274,1279,1283,1286,1288,1292,1295,1297,1300,1303,1306,1310,1313,1318,1322,1326,1330],{"viewBox":1242,"role":42,"ariaLabel":1243,"xmlns":44},"0 0 760 258","Diagram showing a project variable set once in Python flowing into a label expression, a layout title, a data defined symbol size and a layer filter, all reading the same at-sign variable",[46,1245,1246],{},"One variable, read in four places",[50,1248,1249],{},"A single call to set a project variable named survey round writes one value into the project file. Four consumers read it through the expression engine: a feature label, a print layout title block, a data defined symbol size, and a layer subset filter. Changing the value in one place updates all four without editing any of them.",[54,1251],{"x":56,"y":56,"width":57,"height":1252,"fill":59},"258",[61,1254,1255],{},[64,1256,1258],{"id":1257,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"projVarArrow",[73,1259],{"d":75,"fill":93},[78,1261,1262],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"Set it once, read it everywhere",[54,1264],{"x":1265,"y":89,"width":105,"height":1266,"rx":68,"fill":92,"stroke":93,"style":94},"248","62",[78,1268,1270],{"x":80,"y":1269,"style":99,"fill":93,"textAnchor":84},"72","setProjectVariable",[78,1272,1273],{"x":80,"y":522,"style":118,"fill":76,"textAnchor":84},"survey round = 2026-Q3",[54,1275],{"x":1276,"y":1277,"width":847,"height":1278,"rx":68,"fill":107,"stroke":147,"style":514},"16","164","66",[78,1280,1282],{"x":183,"y":1281,"style":113,"fill":147,"textAnchor":84},"188","label expression",[78,1284,1285],{"x":183,"y":866,"style":118,"fill":76,"textAnchor":84},"round shown per feature",[54,1287],{"x":188,"y":1277,"width":847,"height":1278,"rx":68,"fill":107,"stroke":550,"style":514},[78,1289,1291],{"x":1290,"y":1281,"style":113,"fill":550,"textAnchor":84},"284","layout title",[78,1293,1294],{"x":1290,"y":866,"style":118,"fill":76,"textAnchor":84},"printed on every page",[54,1296],{"x":182,"y":1277,"width":160,"height":1278,"rx":68,"fill":107,"stroke":136,"style":514},[78,1298,1299],{"x":830,"y":1281,"style":113,"fill":136,"textAnchor":84},"data-defined size",[78,1301,1302],{"x":830,"y":866,"style":118,"fill":76,"textAnchor":84},"symbology reacts to it",[54,1304],{"x":1305,"y":1277,"width":847,"height":1278,"rx":68,"fill":107,"stroke":108,"style":514},"576",[78,1307,1309],{"x":1308,"y":1281,"style":113,"fill":83,"textAnchor":84},"660","layer filter",[78,1311,1312],{"x":1308,"y":866,"style":118,"fill":76,"textAnchor":84},"only this round loads",[174,1314],{"x1":1315,"y1":112,"x2":106,"y2":1316,"stroke":93,"style":1317},"330","158","stroke-width:2;marker-end:url(#projVarArrow)",[174,1319],{"x1":1320,"y1":112,"x2":1321,"y2":1316,"stroke":93,"style":1317},"360","290",[174,1323],{"x1":1324,"y1":112,"x2":1325,"y2":1316,"stroke":93,"style":1317},"400","466",[174,1327],{"x1":1328,"y1":112,"x2":1329,"y2":1316,"stroke":93,"style":1317},"430","640",[78,1331,1332],{"x":80,"y":1265,"style":118,"fill":108,"textAnchor":84},"Next quarter, one line changes and nothing else has to",[196,1334,1336],{"id":1335},"the-template-project-pattern","The template-project pattern",[14,1338,1339],{},"The most valuable thing projects give an automation pipeline is a starting point that somebody with cartographic judgement produced. Rebuilding a good map in code — every symbol, every label rule, every layout item — is slow to write and worse to maintain. Reading a template and swapping what varies is neither.",[211,1341,1343],{"className":213,"code":1342,"language":215,"meta":216,"style":216},"from qgis.core import QgsProject\n\ndef render_for_region(template_path, region_code, output_path):\n    project = QgsProject()                         # not the singleton\n    if not project.read(template_path):\n        raise RuntimeError(f\"cannot read {template_path}\")\n\n    QgsExpressionContextUtils.setProjectVariable(project, \"region_code\", region_code)\n\n    boundary = project.mapLayersByName(\"Boundary\")[0]\n    boundary.setSubsetString(f\"region = '{region_code}'\")\n\n    project.write(output_path)\n    return project\n",[27,1344,1345,1355,1359,1371,1384,1393,1422,1426,1437,1442,1464,1487,1492,1498],{"__ignoreMap":216},[220,1346,1347,1349,1351,1353],{"class":174,"line":222},[220,1348,226],{"class":225},[220,1350,230],{"class":229},[220,1352,233],{"class":225},[220,1354,236],{"class":229},[220,1356,1357],{"class":174,"line":239},[220,1358,243],{"emptyLinePlaceholder":242},[220,1360,1361,1364,1368],{"class":174,"line":246},[220,1362,1363],{"class":225},"def",[220,1365,1367],{"class":1366},"svObZ"," render_for_region",[220,1369,1370],{"class":229},"(template_path, region_code, output_path):\n",[220,1372,1373,1376,1378,1381],{"class":174,"line":258},[220,1374,1375],{"class":229},"    project ",[220,1377,252],{"class":225},[220,1379,1380],{"class":229}," QgsProject()                         ",[220,1382,1383],{"class":268},"# not the singleton\n",[220,1385,1386,1388,1390],{"class":174,"line":272},[220,1387,999],{"class":225},[220,1389,374],{"class":225},[220,1391,1392],{"class":229}," project.read(template_path):\n",[220,1394,1395,1398,1400,1402,1405,1408,1411,1414,1417,1420],{"class":174,"line":283},[220,1396,1397],{"class":225},"        raise",[220,1399,392],{"class":261},[220,1401,288],{"class":229},[220,1403,1404],{"class":225},"f",[220,1406,1407],{"class":380},"\"cannot read ",[220,1409,1410],{"class":261},"{",[220,1412,1413],{"class":229},"template_path",[220,1415,1416],{"class":261},"}",[220,1418,1419],{"class":380},"\"",[220,1421,400],{"class":229},[220,1423,1424],{"class":174,"line":300},[220,1425,243],{"emptyLinePlaceholder":242},[220,1427,1428,1431,1434],{"class":174,"line":714},[220,1429,1430],{"class":229},"    QgsExpressionContextUtils.setProjectVariable(project, ",[220,1432,1433],{"class":380},"\"region_code\"",[220,1435,1436],{"class":229},", region_code)\n",[220,1438,1440],{"class":174,"line":1439},9,[220,1441,243],{"emptyLinePlaceholder":242},[220,1443,1445,1448,1450,1453,1456,1459,1461],{"class":174,"line":1444},10,[220,1446,1447],{"class":229},"    boundary ",[220,1449,252],{"class":225},[220,1451,1452],{"class":229}," project.mapLayersByName(",[220,1454,1455],{"class":380},"\"Boundary\"",[220,1457,1458],{"class":229},")[",[220,1460,56],{"class":261},[220,1462,1463],{"class":229},"]\n",[220,1465,1467,1470,1472,1475,1477,1480,1482,1485],{"class":174,"line":1466},11,[220,1468,1469],{"class":229},"    boundary.setSubsetString(",[220,1471,1404],{"class":225},[220,1473,1474],{"class":380},"\"region = '",[220,1476,1410],{"class":261},[220,1478,1479],{"class":229},"region_code",[220,1481,1416],{"class":261},[220,1483,1484],{"class":380},"'\"",[220,1486,400],{"class":229},[220,1488,1490],{"class":174,"line":1489},12,[220,1491,243],{"emptyLinePlaceholder":242},[220,1493,1495],{"class":174,"line":1494},13,[220,1496,1497],{"class":229},"    project.write(output_path)\n",[220,1499,1501,1504],{"class":174,"line":1500},14,[220,1502,1503],{"class":225},"    return",[220,1505,1506],{"class":229}," project\n",[14,1508,1509,1511,1512,1514],{},[312,1510,314],{}," The template holds all the styling, labelling and layout work; the script changes only the two things that vary between runs, then writes a per-region project that a human can open and check. Using a fresh ",[27,1513,337],{}," rather than the singleton means this function is safe to call in a loop and inside a running QGIS without disturbing whatever the user has open. Layers are addressed by name here for readability — in a template you control, that is reasonable; in a project you do not, prefer the stable layer id.",[14,1516,1517,1518,1522,1523,1527],{},"From there the layouts inside the project are ready to export, which is where this joins ",[21,1519,1521],{"href":1520},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002F","Automated Map Layout Generation"," and, for one map per feature, ",[21,1524,1526],{"href":1525},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002F","Automating Atlas Map Series",". The pattern generalises well: a template project, a table of parameters, and a loop that writes one project and one PDF per row.",[196,1529,1531],{"id":1530},"project-properties-beyond-the-layers","Project properties beyond the layers",[14,1533,1534],{},"A surprising amount of behaviour that people assume is a QGIS preference is actually stored per project, which means an automated pipeline has to set it or inherit whatever the template had. The coordinate system is the obvious one, and the least forgiving.",[211,1536,1538],{"className":213,"code":1537,"language":215,"meta":216,"style":216},"from qgis.core import QgsCoordinateReferenceSystem, QgsUnitTypes\n\nproject.setCrs(QgsCoordinateReferenceSystem(\"EPSG:27700\"))\nproject.setDistanceUnits(QgsUnitTypes.DistanceMeters)\nproject.setAreaUnits(QgsUnitTypes.AreaSquareMeters)\nproject.setEllipsoid(\"EPSG:7001\")\n",[27,1539,1540,1551,1555,1566,1571,1576],{"__ignoreMap":216},[220,1541,1542,1544,1546,1548],{"class":174,"line":222},[220,1543,226],{"class":225},[220,1545,230],{"class":229},[220,1547,233],{"class":225},[220,1549,1550],{"class":229}," QgsCoordinateReferenceSystem, QgsUnitTypes\n",[220,1552,1553],{"class":174,"line":239},[220,1554,243],{"emptyLinePlaceholder":242},[220,1556,1557,1560,1563],{"class":174,"line":246},[220,1558,1559],{"class":229},"project.setCrs(QgsCoordinateReferenceSystem(",[220,1561,1562],{"class":380},"\"EPSG:27700\"",[220,1564,1565],{"class":229},"))\n",[220,1567,1568],{"class":174,"line":258},[220,1569,1570],{"class":229},"project.setDistanceUnits(QgsUnitTypes.DistanceMeters)\n",[220,1572,1573],{"class":174,"line":272},[220,1574,1575],{"class":229},"project.setAreaUnits(QgsUnitTypes.AreaSquareMeters)\n",[220,1577,1578,1581,1584],{"class":174,"line":283},[220,1579,1580],{"class":229},"project.setEllipsoid(",[220,1582,1583],{"class":380},"\"EPSG:7001\"",[220,1585,400],{"class":229},[14,1587,1588,1590,1591,938,1594,1597,1598,1602,1603,483],{},[312,1589,314],{}," The project CRS is the coordinate system the canvas and layouts display in; individual layers keep their own and are reprojected on the fly, so setting this changes what the map looks like without touching any data. The distance and area units govern what the measure tools and, importantly, expression functions such as ",[27,1592,1593],{},"$area",[27,1595,1596],{},"$length"," return — a project left on degrees will happily report a parcel's area as 0.0000031 and nobody will notice until it reaches a report. The ellipsoid is what makes those measurements ",[1599,1600,1601],"em",{},"ellipsoidal"," rather than planar; setting it to a sensible value is the difference between a length measured across the curved earth and one measured on a flattened projection, which on long features can differ by a percent or more. The interaction between these settings and layer-level transforms is worked through in ",[21,1604,1606],{"href":1605},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems in PyQGIS",[14,1608,1609],{},"Other per-project settings worth knowing about, all reachable through the same generic entry store:",[1611,1612,1613,1624,1630,1636],"ul",{},[1614,1615,1616,1619,1620,1623],"li",{},[312,1617,1618],{},"Snapping and topological editing",", via ",[27,1621,1622],{},"QgsProject.snappingConfig()"," — essential if a script prepares a project for a digitising team, and completely invisible until somebody starts editing.",[1614,1625,1626,1629],{},[312,1627,1628],{},"The default styles and symbology"," applied to newly added layers, so a generated project does not look like random colours.",[1614,1631,1632,1635],{},[312,1633,1634],{},"Macros and Python startup code"," embedded in the project. QGIS asks the user before running them; a project written by an automated job should generally not contain any, because it turns a data file into executable code.",[1614,1637,1638,1641,1642,1645],{},[312,1639,1640],{},"Custom project properties",", written with ",[27,1643,1644],{},"writeEntry()"," under a scope you choose. This is where a plugin should keep per-project state such as \"which layer holds the survey points\" instead of guessing by name each time.",[211,1647,1649],{"className":213,"code":1648,"language":215,"meta":216,"style":216},"project.writeEntry(\"FloodAtlas\", \"\u002FSurveyLayerId\", boundary.id())\nlayer_id, ok = project.readEntry(\"FloodAtlas\", \"\u002FSurveyLayerId\", \"\")\n",[27,1650,1651,1667],{"__ignoreMap":216},[220,1652,1653,1656,1659,1661,1664],{"class":174,"line":222},[220,1654,1655],{"class":229},"project.writeEntry(",[220,1657,1658],{"class":380},"\"FloodAtlas\"",[220,1660,464],{"class":229},[220,1662,1663],{"class":380},"\"\u002FSurveyLayerId\"",[220,1665,1666],{"class":229},", boundary.id())\n",[220,1668,1669,1672,1674,1677,1679,1681,1683,1685,1688],{"class":174,"line":239},[220,1670,1671],{"class":229},"layer_id, ok ",[220,1673,252],{"class":225},[220,1675,1676],{"class":229}," project.readEntry(",[220,1678,1658],{"class":380},[220,1680,464],{"class":229},[220,1682,1663],{"class":380},[220,1684,464],{"class":229},[220,1686,1687],{"class":380},"\"\"",[220,1689,400],{"class":229},[14,1691,1692,1694],{},[312,1693,314],{}," The scope string namespaces the key so two plugins cannot collide, and the read call returns a tuple of value and success flag — checking the flag rather than the value is what distinguishes \"not set\" from \"set to an empty string\". Values are stored in the project XML and therefore travel with the file, which is exactly the property you want for anything describing that specific project and exactly the property you do not want for machine-specific paths.",[14,1696,1697],{},[39,1698,1701,1704,1707,1710,1713,1716,1719,1722,1726,1730,1734,1737,1740,1742,1745,1748,1751,1754,1757,1759,1762,1765,1769,1772,1775,1778,1781,1784],{"viewBox":1699,"role":42,"ariaLabel":1700,"xmlns":44},"0 0 760 250","Comparison of where a setting lives, showing project scope settings that travel with the file, application scope settings that stay on the machine, and layer scope settings that belong to the dataset",[46,1702,1703],{},"Three places a setting can live",[50,1705,1706],{},"Three columns compare storage scopes. Project scope holds the coordinate system, units, snapping and custom entries, and travels inside the project file. Application scope holds installation paths, proxy settings and plugin preferences, and stays on the machine. Layer scope holds styling, joins and field configuration, and follows the layer wherever it is used.",[54,1708],{"x":56,"y":56,"width":57,"height":1709,"fill":59},"250",[78,1711,1712],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"Ask where a setting should travel to, then store it there",[54,1714],{"x":1276,"y":575,"width":815,"height":1715,"rx":91,"fill":92,"stroke":93,"style":94},"182",[78,1717,1718],{"x":802,"y":1269,"style":99,"fill":93,"textAnchor":84},"project scope",[78,1720,1721],{"x":802,"y":827,"style":118,"fill":76,"textAnchor":84},"CRS, units, ellipsoid",[78,1723,1725],{"x":802,"y":1724,"style":118,"fill":76,"textAnchor":84},"116","snapping, relations",[78,1727,1729],{"x":802,"y":1728,"style":118,"fill":76,"textAnchor":84},"136","variables, metadata",[78,1731,1733],{"x":802,"y":1732,"style":118,"fill":76,"textAnchor":84},"156","your writeEntry keys",[54,1735],{"x":794,"y":160,"width":1736,"height":794,"rx":795,"fill":107,"stroke":93,"style":109},"192",[78,1738,1739],{"x":802,"y":811,"style":113,"fill":93,"textAnchor":84},"travels with the file",[54,1741],{"x":105,"y":575,"width":815,"height":1715,"rx":91,"fill":146,"stroke":147,"style":94},[78,1743,1744],{"x":80,"y":1269,"style":99,"fill":147,"textAnchor":84},"application scope",[78,1746,1747],{"x":80,"y":827,"style":118,"fill":76,"textAnchor":84},"install and data paths",[78,1749,1750],{"x":80,"y":1724,"style":118,"fill":76,"textAnchor":84},"proxy, authentication",[78,1752,1753],{"x":80,"y":1728,"style":118,"fill":76,"textAnchor":84},"plugin preferences",[78,1755,1756],{"x":80,"y":1732,"style":118,"fill":76,"textAnchor":84},"QgsSettings keys",[54,1758],{"x":1290,"y":160,"width":1736,"height":794,"rx":795,"fill":107,"stroke":147,"style":109},[78,1760,1761],{"x":80,"y":811,"style":113,"fill":147,"textAnchor":84},"stays on the machine",[54,1763],{"x":1764,"y":575,"width":815,"height":1715,"rx":91,"fill":135,"stroke":136,"style":94},"512",[78,1766,1768],{"x":1767,"y":1269,"style":99,"fill":136,"textAnchor":84},"628","layer scope",[78,1770,1771],{"x":1767,"y":827,"style":118,"fill":76,"textAnchor":84},"symbology and labels",[78,1773,1774],{"x":1767,"y":1724,"style":118,"fill":76,"textAnchor":84},"field aliases, forms",[78,1776,1777],{"x":1767,"y":1728,"style":118,"fill":76,"textAnchor":84},"joins and subset filter",[78,1779,1780],{"x":1767,"y":1732,"style":118,"fill":76,"textAnchor":84},"layer variables",[54,1782],{"x":1783,"y":160,"width":1736,"height":794,"rx":795,"fill":107,"stroke":136,"style":109},"532",[78,1785,1786],{"x":1767,"y":811,"style":113,"fill":136,"textAnchor":84},"follows the layer",[196,1788,1790],{"id":1789},"projects-version-control-and-reproducibility","Projects, version control and reproducibility",[14,1792,1793],{},"Because a project is a text document that points at data rather than containing it, it belongs in version control — and treating it that way changes how a team works. A reviewer can see that a colleague changed the classification breaks on the risk layer, because that is a visible diff. A broken map can be rolled back in a second. A release can be tagged.",[14,1795,1796],{},"Three practices make it work in practice.",[14,1798,1799,1805,1806,1808],{},[312,1800,1801,1802,1804],{},"Prefer ",[27,1803,449],{}," for anything reviewed."," The zipped ",[27,1807,453],{}," is a binary blob as far as version control is concerned; every save produces a completely different file. The plain XML diffs line by line, and the extra size is irrelevant next to the data it references.",[14,1810,1811,1814,1815,483],{},[312,1812,1813],{},"Write projects from a script where you can."," A project generated by the template pattern above is reproducible by definition: the script and its inputs are the source of truth, and the project is a build artefact. That is the same argument that makes generated projects a natural fit for the scheduled workflows in ",[21,1816,1818],{"href":1817},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fschedule-pyqgis-scripts-with-cron\u002F","Schedule PyQGIS Scripts with cron",[14,1820,1821,1824],{},[312,1822,1823],{},"Normalise before comparing."," QGIS writes a few volatile things into the file — window geometry, the last canvas extent, a save timestamp — so two saves of an unchanged project are not byte-identical. When a test needs to assert that a script produced the expected project, compare the parts you care about rather than the whole file:",[211,1826,1828],{"className":213,"code":1827,"language":215,"meta":216,"style":216},"import xml.etree.ElementTree as ET\n\ntree = ET.parse(\"\u002Fdata\u002Fprojects\u002Fflood_atlas.qgs\")\nsources = sorted(el.get(\"source\") for el in tree.iter(\"datasource\")\n                 if el.get(\"source\"))\nlayer_names = sorted(el.findtext(\"layername\") or \"\" for el in tree.iter(\"maplayer\"))\n",[27,1829,1830,1843,1847,1865,1899,1911],{"__ignoreMap":216},[220,1831,1832,1834,1837,1840],{"class":174,"line":222},[220,1833,233],{"class":225},[220,1835,1836],{"class":229}," xml.etree.ElementTree ",[220,1838,1839],{"class":225},"as",[220,1841,1842],{"class":261}," ET\n",[220,1844,1845],{"class":174,"line":239},[220,1846,243],{"emptyLinePlaceholder":242},[220,1848,1849,1852,1854,1857,1860,1863],{"class":174,"line":246},[220,1850,1851],{"class":229},"tree ",[220,1853,252],{"class":225},[220,1855,1856],{"class":261}," ET",[220,1858,1859],{"class":229},".parse(",[220,1861,1862],{"class":380},"\"\u002Fdata\u002Fprojects\u002Fflood_atlas.qgs\"",[220,1864,400],{"class":229},[220,1866,1867,1870,1872,1875,1878,1881,1884,1886,1889,1891,1894,1897],{"class":174,"line":258},[220,1868,1869],{"class":229},"sources ",[220,1871,252],{"class":225},[220,1873,1874],{"class":261}," sorted",[220,1876,1877],{"class":229},"(el.get(",[220,1879,1880],{"class":380},"\"source\"",[220,1882,1883],{"class":229},") ",[220,1885,985],{"class":225},[220,1887,1888],{"class":229}," el ",[220,1890,991],{"class":225},[220,1892,1893],{"class":229}," tree.iter(",[220,1895,1896],{"class":380},"\"datasource\"",[220,1898,400],{"class":229},[220,1900,1901,1904,1907,1909],{"class":174,"line":272},[220,1902,1903],{"class":225},"                 if",[220,1905,1906],{"class":229}," el.get(",[220,1908,1880],{"class":380},[220,1910,1565],{"class":229},[220,1912,1913,1916,1918,1920,1923,1926,1928,1931,1934,1937,1939,1941,1943,1946],{"class":174,"line":283},[220,1914,1915],{"class":229},"layer_names ",[220,1917,252],{"class":225},[220,1919,1874],{"class":261},[220,1921,1922],{"class":229},"(el.findtext(",[220,1924,1925],{"class":380},"\"layername\"",[220,1927,1883],{"class":229},[220,1929,1930],{"class":225},"or",[220,1932,1933],{"class":380}," \"\"",[220,1935,1936],{"class":225}," for",[220,1938,1888],{"class":229},[220,1940,991],{"class":225},[220,1942,1893],{"class":229},[220,1944,1945],{"class":380},"\"maplayer\"",[220,1947,1565],{"class":229},[14,1949,1950,1952,1953,1956,1957,1960,1961,1963,1964,1968],{},[312,1951,314],{}," Reading the XML directly is fine for ",[1599,1954,1955],{},"assertions"," about a project, and much simpler than loading it into a QGIS application inside a test. It is not fine for ",[1599,1958,1959],{},"modifying"," one — that is what ",[27,1962,1042],{}," and the rest of the API exist for, because the internal references between layers, layouts, relations and joins are easy to break with a text edit and produce a project that opens with no visible error and quietly wrong behaviour. The same split applies to the plugin test suites described in ",[21,1965,1967],{"href":1966},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","Unit Test a QGIS Plugin with pytest",": parse to assert, use the API to change.",[14,1970,1971],{},"One consequence worth planning for: a project that is generated should never be edited by hand, and a project that is hand-edited should never be regenerated. Decide which of the two a given file is, say so in the folder, and the team will not lose an afternoon's cartography to a nightly job.",[196,1973,1975],{"id":1974},"key-takeaways","Key takeaways",[1611,1977,1978,1984,1993,2004,2012,2018],{},[1614,1979,1980,1983],{},[312,1981,1982],{},"A project is references and decisions, not data."," It is small, regenerable and version-controllable — and it breaks the moment the data it points at moves.",[1614,1985,1986,315,1989,1992],{},[312,1987,1988],{},"The registry and the layer tree are separate.",[27,1990,1991],{},"addMapLayer(layer, False)"," registers without showing; forgetting the second argument is why scripts cannot control layer order.",[1614,1994,1995,1998,1999,946,2001,2003],{},[312,1996,1997],{},"Store relative paths"," for any project that travels, and set ",[27,2000,945],{},[27,2002,690],{}," before writing rather than fixing it afterwards.",[1614,2005,2006,2011],{},[312,2007,2008,2009],{},"Repair broken layers with ",[27,2010,1042],{},", which keeps the layer id, styling and every reference to it intact.",[1614,2013,2014,2017],{},[312,2015,2016],{},"Project variables are the parameter mechanism."," One assignment reaches labels, layouts, filters and data-defined overrides.",[1614,2019,2020,2023],{},[312,2021,2022],{},"Start from a template project."," Reading a hand-built project and changing what varies beats constructing cartography in code, every time.",[196,2025,2027],{"id":2026},"frequently-asked-questions","Frequently Asked Questions",[14,2029,2030,2033,2034,2036],{},[312,2031,2032],{},"Should I use QgsProject.instance() or create my own project?","\nUse the singleton when the script is meant to act on what the user has open, and a fresh ",[27,2035,337],{}," for anything batch-like. A loop that reads twenty template projects into the singleton will fight with the running application's state; twenty independent project objects will not.",[14,2038,2039,2042,2043,2046,2047,2049,2050,2052],{},[312,2040,2041],{},"Why does my layer not appear in the Layers panel even though it loaded?","\nEither it was never added to the project at all — a ",[27,2044,2045],{},"QgsVectorLayer"," that is only referenced by a Python variable is invisible and will be garbage-collected — or it was added with ",[27,2048,736],{}," set to ",[27,2051,690],{}," and never inserted into the layer tree.",[14,2054,2055,2058,2060,2061,2063,2064,2066,2067,2069,2070,483],{},[312,2056,2057],{},"What is the difference between .qgs and .qgz?",[27,2059,449],{}," is the raw XML document; ",[27,2062,453],{}," is a zip containing that XML plus auxiliary storage. Prefer ",[27,2065,453],{}," for distribution and ",[27,2068,449],{}," when you want a readable diff in version control — the format is chosen purely by the file extension you pass to ",[27,2071,439],{},[14,2073,2074,2077,2078,2081,2082,2084],{},[312,2075,2076],{},"How do I change a data source for every layer at once?","\nIterate ",[27,2079,2080],{},"project.mapLayers().values()"," and call ",[27,2083,1042],{}," on each, rewriting the part of the string that changed. Do it in a script rather than by editing the XML, because layouts, joins and relations all reference layers by id and a text edit is easy to get subtly wrong.",[14,2086,2087,2090,2091,2093,2094,483],{},[312,2088,2089],{},"Can I read a project without a running QGIS application?","\nYou need ",[27,2092,208],{}," initialised, but not a GUI — a standalone script with the offscreen platform reads and writes projects perfectly well, which is the basis of the workflows in ",[21,2095,34],{"href":33},[14,2097,2098,2101,2102,2105],{},[312,2099,2100],{},"Do project variables survive a save?","\nYes — they are written into the project file and reload with it. Variables set on the ",[1599,2103,2104],{},"application"," scope do not; those live in user settings and are per-installation, which is the right home for a machine-specific path.",[196,2107,2109],{"id":2108},"related-guides","Related Guides",[1611,2111,2112,2118,2124,2128,2134,2138,2142,2146],{},[1614,2113,2114,2115,2117],{},"Up: ",[21,2116,24],{"href":23}," — the parent guide for this topic",[1614,2119,2120],{},[21,2121,2123],{"href":2122},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002F","QGIS Python Console Basics",[1614,2125,2126],{},[21,2127,34],{"href":33},[1614,2129,2130],{},[21,2131,2133],{"href":2132},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","Working with QGIS Expressions",[1614,2135,2136],{},[21,2137,482],{"href":481},[1614,2139,2140],{},[21,2141,757],{"href":756},[1614,2143,2144],{},[21,2145,752],{"href":751},[1614,2147,2148],{},[21,2149,1237],{"href":1236},[2151,2152,2153],"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 .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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":216,"searchDepth":239,"depth":239,"links":2155},[2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166],{"id":198,"depth":239,"text":199},{"id":345,"depth":239,"text":346},{"id":602,"depth":239,"text":603},{"id":874,"depth":239,"text":875},{"id":1046,"depth":239,"text":1047},{"id":1335,"depth":239,"text":1336},{"id":1530,"depth":239,"text":1531},{"id":1789,"depth":239,"text":1790},{"id":1974,"depth":239,"text":1975},{"id":2026,"depth":239,"text":2027},{"id":2108,"depth":239,"text":2109},"Read, modify and write .qgs and .qgz projects from Python — the layer registry against the layer tree, relative paths, project variables, metadata, and the template-project pattern that drives most unattended map production.","md",{"slug":2170,"type":2171,"breadcrumb":2172,"datePublished":2173,"dateModified":2173},"working-with-qgis-projects","guide","Working with Projects","2026-08-15","\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects",{"title":5,"description":2167},"pyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Findex","OWFMVIcNluLiXDPSM7AG-UFMQBYbR9bnD7lc1vQyx-0",1786789584633]