[{"data":1,"prerenderedAt":1424},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis":3},{"id":4,"title":5,"body":6,"description":1413,"extension":1414,"meta":1415,"navigation":253,"path":1420,"seo":1421,"stem":1422,"__hash__":1423},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis\u002Findex.md","Save and Load a QGIS Project in PyQGIS",{"type":7,"value":8,"toc":1400},"minimark",[9,13,26,35,186,191,217,221,425,452,456,517,541,544,574,582,586,589,627,636,709,713,720,887,896,900,903,996,1020,1098,1102,1177,1185,1189,1267,1271,1287,1291,1303,1314,1331,1344,1363,1367,1396],[10,11,5],"h1",{"id":12},"save-and-load-a-qgis-project-in-pyqgis",[14,15,16,17,21,22,25],"p",{},"Two methods cover ninety percent of project work — ",[18,19,20],"code",{},"QgsProject.read()"," and ",[18,23,24],{},"QgsProject.write()"," — and both of them fail quietly if you let them. They return a boolean rather than raising, so a script that ignores the result carries on happily with an empty project, produces an empty map, and reports success. Getting this right is four lines of code, and it is the difference between a scheduled job that tells you it broke and one that emails an empty PDF every night for a month.",[14,27,28,29,34],{},"This recipe belongs to ",[30,31,33],"a",{"href":32},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002F","Working with QGIS Projects in PyQGIS",". It covers loading a project from disk, saving it back or to a new path, the relative-versus-absolute path decision, and what to do when a project loads but its layers do not.",[14,36,37],{},[38,39,44,48,52,59,76,85,92,98,103,109,113,116,123,129,134,138,142,147,150,153,156,161,165,168,171,175,178,182],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 268","img","Flow of a project read showing the file being parsed, each layer resolved against its data source, and the three possible outcomes of file missing, layers unavailable, and full success","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"What read() actually does, and where it can go wrong",[49,50,51],"desc",{},"Reading a project parses the XML, then resolves each layer against its stored data source. Three outcomes follow: the file itself is missing or unreadable and read returns false; the file parses but some data sources cannot be reached, in which case read still returns true and the layers are marked invalid; or everything resolves and the project is fully usable.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","268","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"loadFlowArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","read() returning True does not mean the layers arrived",[53,86],{"x":80,"y":87,"width":88,"height":87,"rx":67,"fill":89,"stroke":90,"style":91},"60","164","#fffdf7","#59645f","stroke-width:2",[77,93,97],{"x":94,"y":95,"style":96,"fill":82,"textAnchor":83},"110","84","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","parse the XML",[77,99,102],{"x":94,"y":100,"style":101,"fill":75,"textAnchor":83},"103","text-anchor:middle;font-size:11px;font-family:sans-serif","from .qgs or .qgz",[53,104],{"x":105,"y":87,"width":106,"height":87,"rx":67,"fill":107,"stroke":108,"style":91},"240","180","#eef7f4","#0f766e",[77,110,112],{"x":111,"y":95,"style":96,"fill":108,"textAnchor":83},"330","resolve each layer",[77,114,115],{"x":111,"y":100,"style":101,"fill":75,"textAnchor":83},"open its data source",[117,118],"line",{"x1":119,"y1":120,"x2":121,"y2":120,"stroke":75,"style":122},"192","90","234","stroke-width:2;marker-end:url(#loadFlowArrow)",[53,124],{"x":80,"y":88,"width":125,"height":126,"rx":67,"fill":127,"stroke":128,"style":91},"220","72","#fdf2e2","#b91c1c",[77,130,133],{"x":131,"y":132,"style":96,"fill":128,"textAnchor":83},"138","188","file missing or corrupt",[77,135,137],{"x":131,"y":136,"style":101,"fill":75,"textAnchor":83},"208","read() returns False",[77,139,141],{"x":131,"y":140,"style":101,"fill":75,"textAnchor":83},"226","check it and stop",[53,143],{"x":57,"y":88,"width":144,"height":126,"rx":67,"fill":127,"stroke":145,"style":146},"224","#b45309","stroke-width:2.5",[77,148,149],{"x":79,"y":132,"style":96,"fill":145,"textAnchor":83},"data source unreachable",[77,151,152],{"x":79,"y":136,"style":101,"fill":75,"textAnchor":83},"read() still returns True",[77,154,155],{"x":79,"y":140,"style":101,"fill":75,"textAnchor":83},"layer.isValid() is False",[53,157],{"x":158,"y":88,"width":125,"height":126,"rx":67,"fill":159,"stroke":160,"style":91},"512","#edf8e9","#15803d",[77,162,164],{"x":163,"y":132,"style":96,"fill":160,"textAnchor":83},"622","everything resolves",[77,166,167],{"x":163,"y":136,"style":101,"fill":75,"textAnchor":83},"layers, styles, layouts",[77,169,170],{"x":163,"y":140,"style":101,"fill":75,"textAnchor":83},"all present",[117,172],{"x1":173,"y1":173,"x2":173,"y2":174,"stroke":128,"style":122},"120","158",[117,176],{"x1":111,"y1":173,"x2":177,"y2":174,"stroke":145,"style":122},"360",[117,179],{"x1":180,"y1":173,"x2":181,"y2":174,"stroke":160,"style":122},"400","600",[77,183,185],{"x":79,"y":184,"style":101,"fill":90,"textAnchor":83},"256","The middle case is the dangerous one — it looks like success",[187,188,190],"h2",{"id":189},"prerequisites","Prerequisites",[192,193,194,202,205],"ul",{},[195,196,197,201],"li",{},[198,199,200],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer. Everything here also works on 3.22 and 3.28 with the notes at the end.",[195,203,204],{},"A project file to read, or write permission on the folder you intend to write to.",[195,206,207,208,211,212,216],{},"For standalone scripts, an initialised ",[18,209,210],{},"QgsApplication"," — see ",[30,213,215],{"href":214},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","Running Python Scripts Outside QGIS Desktop",".",[187,218,220],{"id":219},"load-a-project-and-verify-it","Load a project and verify it",[222,223,228],"pre",{"className":224,"code":225,"language":226,"meta":227,"style":227},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nproject = QgsProject.instance()\n\nif not project.read(\"\u002Fdata\u002Fprojects\u002Fflood_atlas.qgz\"):\n    raise RuntimeError(\"could not read the project file\")\n\nprint(project.fileName())\nprint(project.title())\nprint(len(project.mapLayers()), \"layers registered\")\n\nbroken = [layer.name() for layer in project.mapLayers().values() if not layer.isValid()]\nif broken:\n    raise RuntimeError(f\"layers failed to load: {', '.join(broken)}\")\n","python","",[18,229,230,248,255,267,272,291,310,315,324,332,350,355,385,393],{"__ignoreMap":227},[231,232,234,238,242,245],"span",{"class":117,"line":233},1,[231,235,237],{"class":236},"snl16","from",[231,239,241],{"class":240},"s95oV"," qgis.core ",[231,243,244],{"class":236},"import",[231,246,247],{"class":240}," QgsProject\n",[231,249,251],{"class":117,"line":250},2,[231,252,254],{"emptyLinePlaceholder":253},true,"\n",[231,256,258,261,264],{"class":117,"line":257},3,[231,259,260],{"class":240},"project ",[231,262,263],{"class":236},"=",[231,265,266],{"class":240}," QgsProject.instance()\n",[231,268,270],{"class":117,"line":269},4,[231,271,254],{"emptyLinePlaceholder":253},[231,273,275,278,281,284,288],{"class":117,"line":274},5,[231,276,277],{"class":236},"if",[231,279,280],{"class":236}," not",[231,282,283],{"class":240}," project.read(",[231,285,287],{"class":286},"sU2Wk","\"\u002Fdata\u002Fprojects\u002Fflood_atlas.qgz\"",[231,289,290],{"class":240},"):\n",[231,292,294,297,301,304,307],{"class":117,"line":293},6,[231,295,296],{"class":236},"    raise",[231,298,300],{"class":299},"sDLfK"," RuntimeError",[231,302,303],{"class":240},"(",[231,305,306],{"class":286},"\"could not read the project file\"",[231,308,309],{"class":240},")\n",[231,311,313],{"class":117,"line":312},7,[231,314,254],{"emptyLinePlaceholder":253},[231,316,318,321],{"class":117,"line":317},8,[231,319,320],{"class":299},"print",[231,322,323],{"class":240},"(project.fileName())\n",[231,325,327,329],{"class":117,"line":326},9,[231,328,320],{"class":299},[231,330,331],{"class":240},"(project.title())\n",[231,333,335,337,339,342,345,348],{"class":117,"line":334},10,[231,336,320],{"class":299},[231,338,303],{"class":240},[231,340,341],{"class":299},"len",[231,343,344],{"class":240},"(project.mapLayers()), ",[231,346,347],{"class":286},"\"layers registered\"",[231,349,309],{"class":240},[231,351,353],{"class":117,"line":352},11,[231,354,254],{"emptyLinePlaceholder":253},[231,356,358,361,363,366,369,372,375,378,380,382],{"class":117,"line":357},12,[231,359,360],{"class":240},"broken ",[231,362,263],{"class":236},[231,364,365],{"class":240}," [layer.name() ",[231,367,368],{"class":236},"for",[231,370,371],{"class":240}," layer ",[231,373,374],{"class":236},"in",[231,376,377],{"class":240}," project.mapLayers().values() ",[231,379,277],{"class":236},[231,381,280],{"class":236},[231,383,384],{"class":240}," layer.isValid()]\n",[231,386,388,390],{"class":117,"line":387},13,[231,389,277],{"class":236},[231,391,392],{"class":240}," broken:\n",[231,394,396,398,400,402,405,408,411,414,417,420,423],{"class":117,"line":395},14,[231,397,296],{"class":236},[231,399,300],{"class":299},[231,401,303],{"class":240},[231,403,404],{"class":236},"f",[231,406,407],{"class":286},"\"layers failed to load: ",[231,409,410],{"class":299},"{",[231,412,413],{"class":286},"', '",[231,415,416],{"class":240},".join(broken)",[231,418,419],{"class":299},"}",[231,421,422],{"class":286},"\"",[231,424,309],{"class":240},[14,426,427,430,431,434,435,438,439,443,444,447,448,451],{},[198,428,429],{},"Breakdown:"," ",[18,432,433],{},"read()"," replaces whatever the project object currently holds, so there is no need to clear it first — though calling ",[18,436,437],{},"project.clear()"," explicitly makes the intent obvious in a long script. The return value covers only whether the ",[440,441,442],"em",{},"file"," could be opened and parsed. Layers whose data sources cannot be reached do not make it return ",[18,445,446],{},"False","; they load as invalid layer objects that keep their name, id and styling. Checking ",[18,449,450],{},"isValid()"," across the registry is therefore not optional in an unattended job, and raising on a broken layer is nearly always better than rendering a map with a hole in it.",[187,453,455],{"id":454},"save-the-project","Save the project",[222,457,459],{"className":224,"code":458,"language":226,"meta":227,"style":227},"project.setTitle(\"Flood atlas — August 2026\")\n\nif not project.write(\"\u002Fdata\u002Fprojects\u002Fflood_atlas_august.qgz\"):\n    raise RuntimeError(\"could not write the project file\")\n\nprint(project.isDirty())          # False after a successful write\n",[18,460,461,471,475,489,502,506],{"__ignoreMap":227},[231,462,463,466,469],{"class":117,"line":233},[231,464,465],{"class":240},"project.setTitle(",[231,467,468],{"class":286},"\"Flood atlas — August 2026\"",[231,470,309],{"class":240},[231,472,473],{"class":117,"line":250},[231,474,254],{"emptyLinePlaceholder":253},[231,476,477,479,481,484,487],{"class":117,"line":257},[231,478,277],{"class":236},[231,480,280],{"class":236},[231,482,483],{"class":240}," project.write(",[231,485,486],{"class":286},"\"\u002Fdata\u002Fprojects\u002Fflood_atlas_august.qgz\"",[231,488,290],{"class":240},[231,490,491,493,495,497,500],{"class":117,"line":269},[231,492,296],{"class":236},[231,494,300],{"class":299},[231,496,303],{"class":240},[231,498,499],{"class":286},"\"could not write the project file\"",[231,501,309],{"class":240},[231,503,504],{"class":117,"line":274},[231,505,254],{"emptyLinePlaceholder":253},[231,507,508,510,513],{"class":117,"line":293},[231,509,320],{"class":299},[231,511,512],{"class":240},"(project.isDirty())          ",[231,514,516],{"class":515},"sjoCn","# False after a successful write\n",[14,518,519,521,522,525,526,529,530,532,533,536,537,540],{},[198,520,429],{}," Passing a path performs a save-as: the file is written and ",[18,523,524],{},"fileName()"," now points at the new location, so a later bare ",[18,527,528],{},"write()"," saves there rather than to the original. Calling ",[18,531,528],{}," with no arguments saves back over the file that was read — correct for an interactive tool, risky for a scheduled job, because the input and the output become the same file and a half-finished run destroys the template. The extension decides the format: ",[18,534,535],{},".qgz"," produces a zip containing the XML plus auxiliary storage, ",[18,538,539],{},".qgs"," produces the bare XML. A successful write clears the dirty flag, which is a cheap way to assert that the save really happened.",[14,542,543],{},"To save a copy while leaving the user's open project untouched — the usual requirement in a plugin — write to a new path and then set the file name back:",[222,545,547],{"className":224,"code":546,"language":226,"meta":227,"style":227},"original = project.fileName()\nproject.write(\"\u002Fdata\u002Fexports\u002Fsnapshot.qgz\")\nproject.setFileName(original)\n",[18,548,549,559,569],{"__ignoreMap":227},[231,550,551,554,556],{"class":117,"line":233},[231,552,553],{"class":240},"original ",[231,555,263],{"class":236},[231,557,558],{"class":240}," project.fileName()\n",[231,560,561,564,567],{"class":117,"line":250},[231,562,563],{"class":240},"project.write(",[231,565,566],{"class":286},"\"\u002Fdata\u002Fexports\u002Fsnapshot.qgz\"",[231,568,309],{"class":240},[231,570,571],{"class":117,"line":257},[231,572,573],{"class":240},"project.setFileName(original)\n",[14,575,576,430,578,581],{},[198,577,429],{},[18,579,580],{},"setFileName()"," only changes what the project thinks its path is; it writes nothing. Restoring the original path means the user's next Ctrl+S goes where they expect rather than into your export folder — a small courtesy that prevents a genuinely confusing bug report.",[187,583,585],{"id":584},"choose-relative-paths-before-you-save","Choose relative paths before you save",[14,587,588],{},"Whether data sources are written as absolute or relative paths is a property of the project, applied at save time. Set it before writing, not after.",[222,590,592],{"className":224,"code":591,"language":226,"meta":227,"style":227},"project.writeEntryBool(\"Paths\", \"\u002FAbsolute\", False)   # relative to the project folder\nproject.write(\"\u002Fdata\u002Fprojects\u002Fportable\u002Fatlas.qgs\")\n",[18,593,594,618],{"__ignoreMap":227},[231,595,596,599,602,605,608,610,612,615],{"class":117,"line":233},[231,597,598],{"class":240},"project.writeEntryBool(",[231,600,601],{"class":286},"\"Paths\"",[231,603,604],{"class":240},", ",[231,606,607],{"class":286},"\"\u002FAbsolute\"",[231,609,604],{"class":240},[231,611,446],{"class":299},[231,613,614],{"class":240},")   ",[231,616,617],{"class":515},"# relative to the project folder\n",[231,619,620,622,625],{"class":117,"line":250},[231,621,563],{"class":240},[231,623,624],{"class":286},"\"\u002Fdata\u002Fprojects\u002Fportable\u002Fatlas.qgs\"",[231,626,309],{"class":240},[14,628,629,631,632,635],{},[198,630,429],{}," With relative paths, a folder containing the project and its data can be copied to another machine, zipped for a client or committed to a repository and will still open. With absolute paths it works only where it was made. The setting applies to sources written from this point on, so change it ",[440,633,634],{},"before"," the save that matters. Layers backed by a database or a web service are unaffected — their connection strings carry no local path — which is one reason those sources are worth preferring for anything shared.",[14,637,638],{},[38,639,642,645,648,651,654,661,667,675,679,683,686,689,692,696,699,702,704,706],{"viewBox":640,"role":41,"ariaLabel":641,"xmlns":43},"0 0 760 244","Comparison of a project saved with absolute paths breaking when the folder is copied to another machine against the same project saved with relative paths continuing to work",[45,643,644],{},"The same project, copied to another machine",[49,646,647],{},"On the left a project saved with absolute paths refers to a data folder under a specific user's home directory; after copying the folder to a different machine the layers cannot be found. On the right the same project saved with relative paths refers to a data folder beside the project, so the copy opens with every layer intact.",[53,649],{"x":55,"y":55,"width":56,"height":650,"fill":58},"244",[77,652,653],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"One checkbox decides whether the project survives a copy",[53,655],{"x":656,"y":657,"width":658,"height":659,"rx":660,"fill":127,"stroke":128,"style":146},"20","48","348","176","10",[77,662,666],{"x":663,"y":664,"style":665,"fill":128,"textAnchor":83},"194","74","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","absolute paths",[53,668],{"x":669,"y":670,"width":671,"height":672,"rx":673,"fill":89,"stroke":90,"style":674},"40","88","308","42","6","stroke-width:1.5",[77,676,678],{"x":663,"y":677,"style":101,"fill":75,"textAnchor":83},"114","source: \u002Fhome\u002Fana\u002Fwork\u002Fgis\u002Fdata\u002Froads.gpkg",[77,680,682],{"x":663,"y":681,"style":101,"fill":75,"textAnchor":83},"152","copied to a colleague's laptop",[53,684],{"x":669,"y":685,"width":671,"height":672,"rx":673,"fill":58,"stroke":128,"style":91},"166",[77,687,688],{"x":663,"y":119,"style":96,"fill":128,"textAnchor":83},"no such folder — every layer invalid",[53,690],{"x":691,"y":657,"width":658,"height":659,"rx":660,"fill":159,"stroke":160,"style":146},"392",[77,693,695],{"x":694,"y":664,"style":665,"fill":160,"textAnchor":83},"566","relative paths",[53,697],{"x":698,"y":670,"width":671,"height":672,"rx":673,"fill":89,"stroke":90,"style":674},"412",[77,700,701],{"x":694,"y":677,"style":101,"fill":75,"textAnchor":83},"source: .\u002Fdata\u002Froads.gpkg",[77,703,682],{"x":694,"y":681,"style":101,"fill":75,"textAnchor":83},[53,705],{"x":698,"y":685,"width":671,"height":672,"rx":673,"fill":89,"stroke":160,"style":91},[77,707,708],{"x":694,"y":119,"style":96,"fill":160,"textAnchor":83},"opens exactly as it did at home",[187,710,712],{"id":711},"work-on-a-project-without-touching-the-open-one","Work on a project without touching the open one",[14,714,715,716,719],{},"In a plugin, or in any loop that processes several projects, use a standalone ",[18,717,718],{},"QgsProject"," instead of the singleton.",[222,721,723],{"className":224,"code":722,"language":226,"meta":227,"style":227},"from qgis.core import QgsProject\n\nfor region in (\"north\", \"central\", \"south\"):\n    project = QgsProject()                                  # independent instance\n    if not project.read(f\"\u002Fdata\u002Ftemplates\u002F{region}.qgz\"):\n        print(f\"skipping {region}: unreadable\")\n        continue\n\n    project.setTitle(f\"{region.title()} — August 2026\")\n    project.write(f\"\u002Fdata\u002Foutput\u002F{region}_august.qgz\")\n    project.clear()                                          # release layers promptly\n",[18,724,725,735,739,766,779,805,828,833,837,858,879],{"__ignoreMap":227},[231,726,727,729,731,733],{"class":117,"line":233},[231,728,237],{"class":236},[231,730,241],{"class":240},[231,732,244],{"class":236},[231,734,247],{"class":240},[231,736,737],{"class":117,"line":250},[231,738,254],{"emptyLinePlaceholder":253},[231,740,741,743,746,748,751,754,756,759,761,764],{"class":117,"line":257},[231,742,368],{"class":236},[231,744,745],{"class":240}," region ",[231,747,374],{"class":236},[231,749,750],{"class":240}," (",[231,752,753],{"class":286},"\"north\"",[231,755,604],{"class":240},[231,757,758],{"class":286},"\"central\"",[231,760,604],{"class":240},[231,762,763],{"class":286},"\"south\"",[231,765,290],{"class":240},[231,767,768,771,773,776],{"class":117,"line":269},[231,769,770],{"class":240},"    project ",[231,772,263],{"class":236},[231,774,775],{"class":240}," QgsProject()                                  ",[231,777,778],{"class":515},"# independent instance\n",[231,780,781,784,786,788,790,793,795,798,800,803],{"class":117,"line":274},[231,782,783],{"class":236},"    if",[231,785,280],{"class":236},[231,787,283],{"class":240},[231,789,404],{"class":236},[231,791,792],{"class":286},"\"\u002Fdata\u002Ftemplates\u002F",[231,794,410],{"class":299},[231,796,797],{"class":240},"region",[231,799,419],{"class":299},[231,801,802],{"class":286},".qgz\"",[231,804,290],{"class":240},[231,806,807,810,812,814,817,819,821,823,826],{"class":117,"line":293},[231,808,809],{"class":299},"        print",[231,811,303],{"class":240},[231,813,404],{"class":236},[231,815,816],{"class":286},"\"skipping ",[231,818,410],{"class":299},[231,820,797],{"class":240},[231,822,419],{"class":299},[231,824,825],{"class":286},": unreadable\"",[231,827,309],{"class":240},[231,829,830],{"class":117,"line":312},[231,831,832],{"class":236},"        continue\n",[231,834,835],{"class":117,"line":317},[231,836,254],{"emptyLinePlaceholder":253},[231,838,839,842,844,846,848,851,853,856],{"class":117,"line":326},[231,840,841],{"class":240},"    project.setTitle(",[231,843,404],{"class":236},[231,845,422],{"class":286},[231,847,410],{"class":299},[231,849,850],{"class":240},"region.title()",[231,852,419],{"class":299},[231,854,855],{"class":286}," — August 2026\"",[231,857,309],{"class":240},[231,859,860,863,865,868,870,872,874,877],{"class":117,"line":334},[231,861,862],{"class":240},"    project.write(",[231,864,404],{"class":236},[231,866,867],{"class":286},"\"\u002Fdata\u002Foutput\u002F",[231,869,410],{"class":299},[231,871,797],{"class":240},[231,873,419],{"class":299},[231,875,876],{"class":286},"_august.qgz\"",[231,878,309],{"class":240},[231,880,881,884],{"class":117,"line":352},[231,882,883],{"class":240},"    project.clear()                                          ",[231,885,886],{"class":515},"# release layers promptly\n",[14,888,889,891,892,895],{},[198,890,429],{}," Each iteration gets its own project object, so nothing leaks between regions and the user's open project in QGIS is untouched throughout. ",[18,893,894],{},"clear()"," at the end of the loop releases the layers rather than waiting for Python's garbage collector, which matters when the projects are large and the loop is long. Continuing rather than raising on a bad file is a judgement call: for a nightly job that must produce all three, raising is better, because a partial run that reports success is the worst outcome.",[187,897,899],{"id":898},"reacting-to-a-project-being-opened","Reacting to a project being opened",[14,901,902],{},"Plugins usually need to know when a project loads so they can attach to its layers. The signals are on the project object.",[222,904,906],{"className":224,"code":905,"language":226,"meta":227,"style":227},"def on_project_read():\n    project = QgsProject.instance()\n    print(\"opened:\", project.fileName())\n\nQgsProject.instance().readProject.connect(lambda doc: on_project_read())\nQgsProject.instance().layerWasAdded.connect(lambda layer: print(\"added:\", layer.name()))\nQgsProject.instance().cleared.connect(lambda: print(\"project cleared\"))\n",[18,907,908,920,928,941,945,956,976],{"__ignoreMap":227},[231,909,910,913,917],{"class":117,"line":233},[231,911,912],{"class":236},"def",[231,914,916],{"class":915},"svObZ"," on_project_read",[231,918,919],{"class":240},"():\n",[231,921,922,924,926],{"class":117,"line":250},[231,923,770],{"class":240},[231,925,263],{"class":236},[231,927,266],{"class":240},[231,929,930,933,935,938],{"class":117,"line":257},[231,931,932],{"class":299},"    print",[231,934,303],{"class":240},[231,936,937],{"class":286},"\"opened:\"",[231,939,940],{"class":240},", project.fileName())\n",[231,942,943],{"class":117,"line":269},[231,944,254],{"emptyLinePlaceholder":253},[231,946,947,950,953],{"class":117,"line":274},[231,948,949],{"class":240},"QgsProject.instance().readProject.connect(",[231,951,952],{"class":236},"lambda",[231,954,955],{"class":240}," doc: on_project_read())\n",[231,957,958,961,963,966,968,970,973],{"class":117,"line":293},[231,959,960],{"class":240},"QgsProject.instance().layerWasAdded.connect(",[231,962,952],{"class":236},[231,964,965],{"class":240}," layer: ",[231,967,320],{"class":299},[231,969,303],{"class":240},[231,971,972],{"class":286},"\"added:\"",[231,974,975],{"class":240},", layer.name()))\n",[231,977,978,981,983,986,988,990,993],{"class":117,"line":312},[231,979,980],{"class":240},"QgsProject.instance().cleared.connect(",[231,982,952],{"class":236},[231,984,985],{"class":240},": ",[231,987,320],{"class":299},[231,989,303],{"class":240},[231,991,992],{"class":286},"\"project cleared\"",[231,994,995],{"class":240},"))\n",[14,997,998,430,1000,1003,1004,1007,1008,1011,1012,1015,1016,216],{},[198,999,429],{},[18,1001,1002],{},"readProject"," fires after the XML has been parsed and passes the document, which is how a plugin reads its own custom entries out of the file. ",[18,1005,1006],{},"layerWasAdded"," fires once per layer, including during a project load, which makes it the right place to attach per-layer behaviour without enumerating the registry. ",[18,1009,1010],{},"cleared"," fires on New Project and immediately before a read replaces the contents — the signal to tear down anything holding references to layers that are about to disappear. Disconnect these in your plugin's ",[18,1013,1014],{},"unload()",", or a reloaded plugin will handle every event twice; the pattern is covered in ",[30,1017,1019],{"href":1018},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002F","Connect Layer Signals in PyQGIS",[14,1021,1022],{},[38,1023,1026,1029,1032,1035,1042,1045,1050,1055,1058,1063,1067,1070,1072,1075,1078,1081,1083,1086,1089,1092,1095],{"viewBox":1024,"role":41,"ariaLabel":1025,"xmlns":43},"0 0 760 236","Sequence of signals emitted while a project is opened, from cleared through readProject and one layerWasAdded per layer to the final project read state",[45,1027,1028],{},"The order signals arrive when a project opens",[49,1030,1031],{},"A left-to-right sequence shows the cleared signal firing first as the previous project is discarded, then layerWasAdded firing once for each layer as it is registered, then readProject firing after the document has been parsed, and finally the project being fully usable. A note marks that reading custom project entries belongs in the readProject handler.",[53,1033],{"x":55,"y":55,"width":56,"height":1034,"fill":58},"236",[60,1036,1037],{},[63,1038,1040],{"id":1039,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"loadSigArrow",[72,1041],{"d":74,"fill":75},[77,1043,1044],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Attach to the right moment, not the first one",[117,1046],{"x1":669,"y1":1047,"x2":1048,"y2":1047,"stroke":90,"style":1049},"106","726","stroke-width:2;marker-end:url(#loadSigArrow)",[1051,1052],"circle",{"cx":1053,"cy":1047,"r":1054,"fill":145},"96","9",[77,1056,1010],{"x":1053,"y":1057,"style":96,"fill":145,"textAnchor":83},"82",[77,1059,1062],{"x":1053,"y":1060,"style":1061,"fill":90,"textAnchor":83},"136","text-anchor:middle;font-size:10px;font-family:sans-serif","drop references",[1051,1064],{"cx":1065,"cy":1047,"r":1054,"fill":1066},"272","#2563eb",[1051,1068],{"cx":1069,"cy":1047,"r":1054,"fill":1066},"336",[1051,1071],{"cx":180,"cy":1047,"r":1054,"fill":1066},[77,1073,1074],{"x":1069,"y":1057,"style":96,"fill":1066,"textAnchor":83},"layerWasAdded, once per layer",[77,1076,1077],{"x":1069,"y":1060,"style":1061,"fill":90,"textAnchor":83},"attach per-layer behaviour here",[1051,1079],{"cx":1080,"cy":1047,"r":1054,"fill":108},"560",[77,1082,1002],{"x":1080,"y":1057,"style":96,"fill":108,"textAnchor":83},[77,1084,1085],{"x":1080,"y":1060,"style":1061,"fill":90,"textAnchor":83},"read your custom entries",[1051,1087],{"cx":1088,"cy":1047,"r":1054,"fill":160},"686",[77,1090,1091],{"x":1088,"y":1057,"style":96,"fill":160,"textAnchor":83},"usable",[53,1093],{"x":106,"y":685,"width":180,"height":1094,"rx":67,"fill":127,"stroke":145,"style":91},"46",[77,1096,1097],{"x":79,"y":663,"style":101,"fill":75,"textAnchor":83},"Disconnect all of these in unload() or a reloaded plugin reacts twice",[187,1099,1101],{"id":1100},"qgis-version-compatibility","QGIS version compatibility",[1103,1104,1105,1121],"table",{},[1106,1107,1108],"thead",{},[1109,1110,1111,1115,1118],"tr",{},[1112,1113,1114],"th",{},"QGIS version",[1112,1116,1117],{},"Python",[1112,1119,1120],{},"Notes",[1122,1123,1124,1142,1152,1167],"tbody",{},[1109,1125,1126,1130,1133],{},[1127,1128,1129],"td",{},"3.22 LTR",[1127,1131,1132],{},"3.9",[1127,1134,1135,604,1137,604,1139,1141],{},[18,1136,433],{},[18,1138,528],{},[18,1140,535],{}," and the signals above all behave as described.",[1109,1143,1144,1147,1149],{},[1127,1145,1146],{},"3.28 LTR",[1127,1148,1132],{},[1127,1150,1151],{},"Identical.",[1109,1153,1154,1157,1160],{},[1127,1155,1156],{},"3.34 LTR",[1127,1158,1159],{},"3.12",[1127,1161,1162,1163,1166],{},"Baseline for this page. ",[18,1164,1165],{},"setPresetHomePath()"," available for overriding the project folder.",[1109,1168,1169,1172,1174],{},[1127,1170,1171],{},"3.40 \u002F 3.44",[1127,1173,1159],{},[1127,1175,1176],{},"Identical API; project models and the Layouts panel gained features that do not affect these calls.",[14,1178,1179,1180,1184],{},"The one thing to check across versions is not the API but the file format: a project saved by a newer QGIS may use elements an older one ignores, so styling can silently degrade when a 3.44 project is opened in 3.22. Where a project must be readable by an older release, produce it with that release — the compatibility notes in ",[30,1181,1183],{"href":1182},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-python-version-compatibility-guide\u002F","QGIS Python Version Compatibility Guide"," apply to project files just as much as to code.",[187,1186,1188],{"id":1187},"troubleshooting","Troubleshooting",[192,1190,1191,1210,1223,1235,1244,1253],{},[195,1192,1193,1202,1203,1205,1206,1209],{},[198,1194,1195,1197,1198,1201],{},[18,1196,433],{}," returns ",[18,1199,1200],{},"True"," but the map is empty."," The file parsed and the layers did not resolve. Iterate the registry checking ",[18,1204,450],{}," and print ",[18,1207,1208],{},"layer.source()"," for the failures — it is almost always a moved folder or a network share that is not mounted.",[195,1211,1212,1218,1219,1222],{},[198,1213,1214,1197,1216,216],{},[18,1215,528],{},[18,1217,446],{}," The target directory does not exist, or is not writable by the user running the script. QGIS does not create intermediate folders; ",[18,1220,1221],{},"os.makedirs(folder, exist_ok=True)"," first.",[195,1224,1225,1228,1229,1232,1233,216],{},[198,1226,1227],{},"The saved project has absolute paths despite the setting."," The entry was written after the save. Set ",[18,1230,1231],{},"Paths\u002FAbsolute"," before calling ",[18,1234,528],{},[195,1236,1237,1240,1241,1243],{},[198,1238,1239],{},"A standalone script exits with the project unsaved."," There is no prompt outside the GUI. Call ",[18,1242,528],{}," explicitly, and check the return value.",[195,1245,1246,1249,1250,1252],{},[198,1247,1248],{},"Layers appear twice after loading."," The project was read into an instance that already had layers and ",[18,1251,894],{}," was not called — or the same project was read twice into the singleton.",[195,1254,1255,1258,1259,1262,1263,1266],{},[198,1256,1257],{},"The project opens fine in QGIS but not in the script."," The script's QGIS is missing a provider the project uses, such as ",[18,1260,1261],{},"postgres"," in a minimal container. Print ",[18,1264,1265],{},"QgsProviderRegistry.instance().providerList()"," to confirm.",[187,1268,1270],{"id":1269},"conclusion","Conclusion",[14,1272,1273,1274,1276,1277,1279,1280,1283,1284,1286],{},"Read with ",[18,1275,433],{},", write with ",[18,1278,528],{},", and check both return values. Verify layer validity separately, because a project can load perfectly while its data cannot. Decide on relative paths before saving anything that will travel, use a standalone ",[18,1281,1282],{},"QgsProject()"," for batch work so the user's open project is never disturbed, and hook ",[18,1285,1002],{}," rather than polling if a plugin needs to react to project changes.",[187,1288,1290],{"id":1289},"frequently-asked-questions","Frequently Asked Questions",[14,1292,1293,1299,1300,1302],{},[198,1294,1295,1296,1298],{},"Does ",[18,1297,528],{}," with no arguments overwrite the original file?","\nYes — it saves to whatever ",[18,1301,524],{}," currently holds. In a scheduled job always pass an explicit output path so the input template can never be destroyed by a partial run.",[14,1304,1305,1308,1310,1311,1313],{},[198,1306,1307],{},"Should I use .qgs or .qgz?",[18,1309,535],{}," for distribution: one file, smaller, and it carries auxiliary storage. ",[18,1312,539],{}," when the project is reviewed or version-controlled, because plain XML produces a readable diff.",[14,1315,1316,1319,1320,1322,1323,1325,1326,1330],{},[198,1317,1318],{},"How do I open a project without a GUI?","\nInitialise ",[18,1321,210],{}," with the offscreen platform and call ",[18,1324,433],{}," exactly as shown. See ",[30,1327,1329],{"href":1328},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Frun-pyqgis-in-docker-container\u002F","Run PyQGIS in a Docker Container"," for a complete setup.",[14,1332,1333,1336,1337,1339,1340,1343],{},[198,1334,1335],{},"Can I load only some layers from a project?","\nNot selectively through ",[18,1338,433],{},". Read the whole project and remove what you do not need with ",[18,1341,1342],{},"removeMapLayer()",", or read the source strings out of the XML and build only the layers you want.",[14,1345,1346,1356,1357,1359,1360,1362],{},[198,1347,1348,1349,1352,1353,1355],{},"Why is ",[18,1350,1351],{},"isDirty()"," still ",[18,1354,1200],{}," after saving?","\nSomething changed the project after the write — often a signal handler reacting to the save itself. Check ",[18,1358,1351],{}," immediately after ",[18,1361,528],{}," returns to distinguish a failed save from a later modification.",[187,1364,1366],{"id":1365},"related","Related",[192,1368,1369,1374,1380,1386,1392],{},[195,1370,1371,1373],{},[30,1372,33],{"href":32}," — the guide this recipe belongs to",[195,1375,1376],{},[30,1377,1379],{"href":1378},"\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",[195,1381,1382],{},[30,1383,1385],{"href":1384},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Forganise-layer-tree-groups-pyqgis\u002F","Organise the Layer Tree with Groups in PyQGIS",[195,1387,1388],{},[30,1389,1391],{"href":1390},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","Headless QGIS and Server Automation",[195,1393,1394],{},[30,1395,215],{"href":214},[1397,1398,1399],"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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":227,"searchDepth":250,"depth":250,"links":1401},[1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412],{"id":189,"depth":250,"text":190},{"id":219,"depth":250,"text":220},{"id":454,"depth":250,"text":455},{"id":584,"depth":250,"text":585},{"id":711,"depth":250,"text":712},{"id":898,"depth":250,"text":899},{"id":1100,"depth":250,"text":1101},{"id":1187,"depth":250,"text":1188},{"id":1269,"depth":250,"text":1270},{"id":1289,"depth":250,"text":1290},{"id":1365,"depth":250,"text":1366},"Read and write .qgs and .qgz projects from Python — checking return values, choosing relative paths, saving a copy without disturbing the open project, and recovering when layers fail to resolve.","md",{"slug":1416,"type":1417,"breadcrumb":1418,"datePublished":1419,"dateModified":1419},"save-and-load-qgis-project-pyqgis","article","Save & Load a Project","2026-08-15","\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis",{"title":5,"description":1413},"pyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis\u002Findex","OdOAn_oAIivwdane38H9fJdeWKsBOk6zaurx903kaHg",1786789584634]