[{"data":1,"prerenderedAt":1785},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series":3},{"id":4,"title":5,"body":6,"description":1778,"extension":1779,"meta":1780,"navigation":308,"path":1781,"seo":1782,"stem":1783,"__hash__":1784},"docs\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Findex.md","Automating Atlas Map Series in PyQGIS",{"type":7,"value":8,"toc":1764},"minimark",[9,13,23,36,41,86,90,93,257,261,276,469,500,504,515,659,691,695,706,741,774,778,784,884,918,922,937,1189,1232,1236,1242,1474,1507,1511,1572,1582,1586,1644,1648,1669,1685,1706,1718,1736,1740,1760],[10,11,5],"h1",{"id":12},"automating-atlas-map-series-in-pyqgis",[14,15,16,17,22],"p",{},"An atlas turns a single print layout into a series of maps — one page per feature in a coverage layer. Instead of designing forty district maps by hand, you design one and let QGIS iterate. PyQGIS takes this further: you configure and export the entire series from code, with no dialogs, so map books regenerate automatically whenever the underlying data changes. Within ",[18,19,21],"a",{"href":20},"\u002Fspatial-data-processing-automation\u002F","Spatial Data Processing & Automation with PyQGIS",", atlas automation is the publishing endpoint — the step that turns processed data into a stack of finished, paginated maps.",[14,24,25,26,30,31,35],{},"This guide covers the full atlas object model in PyQGIS: attaching an atlas to a ",[27,28,29],"code",{},"QgsLayout",", setting the coverage layer, naming and filtering pages, controlling sort order, governing per-page extent, and exporting the series to a single PDF, one PDF per feature, or a folder of images. It is the conceptual companion to the focused recipe on ",[18,32,34],{"href":33},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fgenerate-atlas-pdf-pyqgis\u002F","generating an atlas PDF in PyQGIS",".",[37,38,40],"h2",{"id":39},"prerequisites","Prerequisites",[42,43,44,52,71,74],"ul",{},[45,46,47,51],"li",{},[48,49,50],"strong",{},"QGIS 3.34 LTR"," (Python 3.12) recommended; the API is stable on 3.28 LTR and the 3.40\u002F3.44 line.",[45,53,54,55,58,59,62,63,66,67,70],{},"A QGIS project (",[27,56,57],{},".qgz","\u002F",[27,60,61],{},".qgs",") containing a ",[48,64,65],{},"print layout"," with at least one map item, plus a ",[48,68,69],{},"coverage layer"," — the polygon (or point) layer whose features become atlas pages.",[45,72,73],{},"The map item set to \"controlled by atlas\" so its extent follows each coverage feature.",[45,75,76,77,80,81,85],{},"Comfort with ",[27,78,79],{},"QgsProject"," and the layout classes from ",[18,82,84],{"href":83},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002F","Automated Map Layout Generation with PyQGIS",", which this guide extends from single layouts to multi-page series.",[37,87,89],{"id":88},"the-atlas-iteration-loop","The Atlas Iteration Loop",[14,91,92],{},"Conceptually an atlas is a loop. QGIS reads the coverage layer, applies a filter and a sort, and for each surviving feature it sets the controlled map's extent, evaluates page-level expressions (titles, page names), renders the layout, and emits output. Understanding this loop is what lets you configure each stage correctly from code.",[94,95,100,101,100,105,100,109,100,116,100,125,100,133,100,139,100,146,100,152,100,156,100,159,100,163,100,167,100,170,100,174,100,178,100,183,100,188,100,191,100,196,100,201,100,205,100,212,100,216,100,220,100,226,100,231,100,237,100,241],"svg",{"viewBox":96,"role":97,"ariaLabel":98,"xmlns":99},"0 0 720 320","img","Diagram of the QGIS atlas iteration loop","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[102,103,104],"title",{},"Atlas iteration loop",[106,107,108],"desc",{},"The coverage layer is filtered and sorted, then for each feature the atlas sets the map extent, evaluates page expressions, renders the layout, and exports a page; the loop repeats until features are exhausted.",[110,111],"rect",{"x":112,"y":112,"width":113,"height":114,"fill":115},"0","720","320","#f6f3ea",[110,117],{"x":118,"y":119,"width":119,"height":120,"rx":121,"fill":122,"stroke":123,"style":124},"30","130","60","6","#fffdf7","#0f766e","stroke-width:2.5",[126,127,132],"text",{"x":128,"y":129,"fill":130,"style":131},"95","155","#17211d","text-anchor:middle;font-family:sans-serif;font-size:13px;font-weight:bold","Coverage layer",[126,134,138],{"x":128,"y":135,"fill":136,"style":137},"173","#2f3b35","text-anchor:middle;font-family:sans-serif;font-size:11px","filter + sort",[110,140],{"x":141,"y":142,"width":143,"height":144,"rx":121,"fill":122,"stroke":145,"style":124},"210","40","140","56","#2563eb",[126,147,151],{"x":148,"y":149,"fill":130,"style":150},"280","65","text-anchor:middle;font-family:sans-serif;font-size:12px;font-weight:bold","Next feature",[126,153,155],{"x":148,"y":154,"fill":136,"style":137},"82","set map extent",[110,157],{"x":141,"y":158,"width":143,"height":144,"rx":121,"fill":122,"stroke":145,"style":124},"135",[126,160,162],{"x":148,"y":161,"fill":130,"style":150},"158","Evaluate",[126,164,166],{"x":148,"y":165,"fill":136,"style":137},"175","page name \u002F labels",[110,168],{"x":141,"y":169,"width":143,"height":144,"rx":121,"fill":122,"stroke":145,"style":124},"230",[126,171,173],{"x":148,"y":172,"fill":130,"style":150},"253","Render layout",[126,175,177],{"x":148,"y":176,"fill":136,"style":137},"270","one page",[110,179],{"x":180,"y":158,"width":143,"height":144,"rx":121,"fill":181,"stroke":182,"style":124},"430","#26322d","#15803d",[126,184,187],{"x":185,"y":161,"fill":186,"style":150},"500","#d9f99d","Export page",[126,189,190],{"x":185,"y":165,"fill":186,"style":137},"PDF \u002F image",[110,192],{"x":193,"y":158,"width":194,"height":144,"rx":121,"fill":122,"stroke":195,"style":124},"620","80","#b45309",[126,197,200],{"x":198,"y":199,"fill":130,"style":150},"660","160","More?",[126,202,204],{"x":198,"y":203,"fill":136,"style":137},"177","yes \u002F no",[206,207],"line",{"x1":199,"y1":208,"x2":209,"y2":210,"stroke":136,"style":211},"150","208","68","stroke-width:2.5;marker-end:url(#b)",[206,213],{"x1":148,"y1":214,"x2":148,"y2":215,"stroke":136,"style":211},"96","133",[206,217],{"x1":148,"y1":218,"x2":148,"y2":219,"stroke":136,"style":211},"191","228",[206,221],{"x1":222,"y1":223,"x2":224,"y2":225,"stroke":136,"style":211},"350","250","428","180",[206,227],{"x1":228,"y1":229,"x2":230,"y2":229,"stroke":136,"style":211},"570","163","618",[232,233],"path",{"d":234,"fill":235,"stroke":195,"style":236},"M660 135 L660 60 L352 60","none","stroke-width:2.5;stroke-dasharray:6 4;marker-end:url(#b)",[126,238,240],{"x":185,"y":239,"fill":195,"style":137},"50","loop: yes",[242,243,244,245,100],"defs",{},"\n    ",[246,247,253,254,244],"marker",{"id":248,"markerWidth":249,"markerHeight":249,"refX":250,"refY":251,"orient":252},"b","9","7","4","auto","\n      ",[232,255],{"d":256,"fill":136},"M0,0 L8,4 L0,8 Z",[37,258,260],{"id":259},"accessing-the-atlas-object","Accessing the Atlas Object",[14,262,263,264,267,268,271,272,275],{},"Every ",[27,265,266],{},"QgsPrintLayout"," owns a ",[27,269,270],{},"QgsLayoutAtlas",", reachable with ",[27,273,274],{},"layout.atlas()",". From there you enable it and bind a coverage layer. This is the foundation every later configuration step builds on.",[277,278,283],"pre",{"className":279,"code":280,"language":281,"meta":282,"style":282},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nproject = QgsProject.instance()\nlayout = project.layoutManager().layoutByName(\"DistrictMaps\")\nif layout is None:\n    raise ValueError(\"Layout 'DistrictMaps' not found in project\")\n\natlas = layout.atlas()\natlas.setEnabled(True)\n\ncoverage = project.mapLayersByName(\"districts\")[0]\natlas.setCoverageLayer(coverage)\n\nprint(f\"Atlas enabled with {coverage.featureCount()} candidate pages\")\n","python","",[27,284,285,303,310,322,340,359,376,381,392,403,408,430,436,441],{"__ignoreMap":282},[286,287,289,293,297,300],"span",{"class":206,"line":288},1,[286,290,292],{"class":291},"snl16","from",[286,294,296],{"class":295},"s95oV"," qgis.core ",[286,298,299],{"class":291},"import",[286,301,302],{"class":295}," QgsProject\n",[286,304,306],{"class":206,"line":305},2,[286,307,309],{"emptyLinePlaceholder":308},true,"\n",[286,311,313,316,319],{"class":206,"line":312},3,[286,314,315],{"class":295},"project ",[286,317,318],{"class":291},"=",[286,320,321],{"class":295}," QgsProject.instance()\n",[286,323,325,328,330,333,337],{"class":206,"line":324},4,[286,326,327],{"class":295},"layout ",[286,329,318],{"class":291},[286,331,332],{"class":295}," project.layoutManager().layoutByName(",[286,334,336],{"class":335},"sU2Wk","\"DistrictMaps\"",[286,338,339],{"class":295},")\n",[286,341,343,346,349,352,356],{"class":206,"line":342},5,[286,344,345],{"class":291},"if",[286,347,348],{"class":295}," layout ",[286,350,351],{"class":291},"is",[286,353,355],{"class":354},"sDLfK"," None",[286,357,358],{"class":295},":\n",[286,360,362,365,368,371,374],{"class":206,"line":361},6,[286,363,364],{"class":291},"    raise",[286,366,367],{"class":354}," ValueError",[286,369,370],{"class":295},"(",[286,372,373],{"class":335},"\"Layout 'DistrictMaps' not found in project\"",[286,375,339],{"class":295},[286,377,379],{"class":206,"line":378},7,[286,380,309],{"emptyLinePlaceholder":308},[286,382,384,387,389],{"class":206,"line":383},8,[286,385,386],{"class":295},"atlas ",[286,388,318],{"class":291},[286,390,391],{"class":295}," layout.atlas()\n",[286,393,395,398,401],{"class":206,"line":394},9,[286,396,397],{"class":295},"atlas.setEnabled(",[286,399,400],{"class":354},"True",[286,402,339],{"class":295},[286,404,406],{"class":206,"line":405},10,[286,407,309],{"emptyLinePlaceholder":308},[286,409,411,414,416,419,422,425,427],{"class":206,"line":410},11,[286,412,413],{"class":295},"coverage ",[286,415,318],{"class":291},[286,417,418],{"class":295}," project.mapLayersByName(",[286,420,421],{"class":335},"\"districts\"",[286,423,424],{"class":295},")[",[286,426,112],{"class":354},[286,428,429],{"class":295},"]\n",[286,431,433],{"class":206,"line":432},12,[286,434,435],{"class":295},"atlas.setCoverageLayer(coverage)\n",[286,437,439],{"class":206,"line":438},13,[286,440,309],{"emptyLinePlaceholder":308},[286,442,444,447,449,452,455,458,461,464,467],{"class":206,"line":443},14,[286,445,446],{"class":354},"print",[286,448,370],{"class":295},[286,450,451],{"class":291},"f",[286,453,454],{"class":335},"\"Atlas enabled with ",[286,456,457],{"class":354},"{",[286,459,460],{"class":295},"coverage.featureCount()",[286,462,463],{"class":354},"}",[286,465,466],{"class":335}," candidate pages\"",[286,468,339],{"class":295},[14,470,471,474,475,478,479,482,483,485,486,488,489,492,493,496,497,35],{},[48,472,473],{},"Breakdown:"," ",[27,476,477],{},"layoutManager().layoutByName(...)"," returns the named print layout or ",[27,480,481],{},"None",", so the guard matters. ",[27,484,274],{}," returns the layout's single ",[27,487,270],{},"; ",[27,490,491],{},"setEnabled(True)"," activates iteration. ",[27,494,495],{},"setCoverageLayer(coverage)"," tells the atlas which layer's features become pages — at this point page count equals the coverage feature count, before any filter is applied. The coverage layer must already be loaded in the project, hence ",[27,498,499],{},"mapLayersByName",[37,501,503],{"id":502},"filtering-and-sorting-pages","Filtering and Sorting Pages",[14,505,506,507,510,511,514],{},"You rarely want every feature. ",[27,508,509],{},"setFilterFeatures(True)"," plus ",[27,512,513],{},"setFilterExpression(...)"," restricts the series to features matching a QGIS expression, and the sort settings control page order — essential for a logical map book.",[277,516,518],{"className":279,"code":517,"language":281,"meta":282,"style":282},"# Only include districts flagged active, ordered alphabetically by name\natlas.setFilterFeatures(True)\nok = atlas.setFilterExpression('\"status\" = \\'active\\' AND \"population\" > 1000')\nif not atlas.filterExpression() or not ok:\n    print(\"Filter expression invalid:\", atlas.filterExpression())\n\natlas.setSortFeatures(True)\natlas.setSortExpression('\"district_name\"')\natlas.setSortAscending(True)\n\natlas.updateFeatures()   # recompute the page list after changing filter\u002Fsort\nprint(f\"Series now has {atlas.count()} pages\")\n",[27,519,520,526,535,561,579,592,596,605,615,624,628,636],{"__ignoreMap":282},[286,521,522],{"class":206,"line":288},[286,523,525],{"class":524},"sAwPA","# Only include districts flagged active, ordered alphabetically by name\n",[286,527,528,531,533],{"class":206,"line":305},[286,529,530],{"class":295},"atlas.setFilterFeatures(",[286,532,400],{"class":354},[286,534,339],{"class":295},[286,536,537,540,542,545,548,551,554,556,559],{"class":206,"line":312},[286,538,539],{"class":295},"ok ",[286,541,318],{"class":291},[286,543,544],{"class":295}," atlas.setFilterExpression(",[286,546,547],{"class":335},"'\"status\" = ",[286,549,550],{"class":354},"\\'",[286,552,553],{"class":335},"active",[286,555,550],{"class":354},[286,557,558],{"class":335}," AND \"population\" > 1000'",[286,560,339],{"class":295},[286,562,563,565,568,571,574,576],{"class":206,"line":324},[286,564,345],{"class":291},[286,566,567],{"class":291}," not",[286,569,570],{"class":295}," atlas.filterExpression() ",[286,572,573],{"class":291},"or",[286,575,567],{"class":291},[286,577,578],{"class":295}," ok:\n",[286,580,581,584,586,589],{"class":206,"line":342},[286,582,583],{"class":354},"    print",[286,585,370],{"class":295},[286,587,588],{"class":335},"\"Filter expression invalid:\"",[286,590,591],{"class":295},", atlas.filterExpression())\n",[286,593,594],{"class":206,"line":361},[286,595,309],{"emptyLinePlaceholder":308},[286,597,598,601,603],{"class":206,"line":378},[286,599,600],{"class":295},"atlas.setSortFeatures(",[286,602,400],{"class":354},[286,604,339],{"class":295},[286,606,607,610,613],{"class":206,"line":383},[286,608,609],{"class":295},"atlas.setSortExpression(",[286,611,612],{"class":335},"'\"district_name\"'",[286,614,339],{"class":295},[286,616,617,620,622],{"class":206,"line":394},[286,618,619],{"class":295},"atlas.setSortAscending(",[286,621,400],{"class":354},[286,623,339],{"class":295},[286,625,626],{"class":206,"line":405},[286,627,309],{"emptyLinePlaceholder":308},[286,629,630,633],{"class":206,"line":410},[286,631,632],{"class":295},"atlas.updateFeatures()   ",[286,634,635],{"class":524},"# recompute the page list after changing filter\u002Fsort\n",[286,637,638,640,642,644,647,649,652,654,657],{"class":206,"line":432},[286,639,446],{"class":354},[286,641,370],{"class":295},[286,643,451],{"class":291},[286,645,646],{"class":335},"\"Series now has ",[286,648,457],{"class":354},[286,650,651],{"class":295},"atlas.count()",[286,653,463],{"class":354},[286,655,656],{"class":335}," pages\"",[286,658,339],{"class":295},[14,660,661,474,663,665,666,668,669,672,673,672,676,679,680,683,684,687,688,690],{},[48,662,473],{},[27,664,509],{}," switches filtering on; ",[27,667,513],{}," accepts any valid QGIS expression referencing coverage-layer fields. In newer QGIS the setter returns a bool indicating a valid expression — always confirm, because an invalid filter silently yields zero pages. ",[27,670,671],{},"setSortFeatures"," \u002F ",[27,674,675],{},"setSortExpression",[27,677,678],{},"setSortAscending"," define order. Crucially, ",[27,681,682],{},"updateFeatures()"," recomputes the filtered, sorted page list; without it ",[27,685,686],{},"count()"," may report stale numbers. Use ",[27,689,651],{}," to verify you have the pages you expect before exporting.",[37,692,694],{"id":693},"naming-pages-and-driving-dynamic-content","Naming Pages and Driving Dynamic Content",[14,696,697,698,701,702,705],{},"The ",[27,699,700],{},"pageNameExpression"," gives each page a meaningful name, used for per-feature output filenames and accessible to layout labels. Combined with the ",[27,703,704],{},"@atlas_*"," expression variables, this is how titles and stats update per page.",[277,707,709],{"className":279,"code":708,"language":281,"meta":282,"style":282},"# Page name like \"03_Riverside\" — zero-padded index plus district name\natlas.setPageNameExpression(\n    \"lpad(@atlas_featurenumber, 2, '0') || '_' || \\\"district_name\\\"\"\n)\n",[27,710,711,716,721,737],{"__ignoreMap":282},[286,712,713],{"class":206,"line":288},[286,714,715],{"class":524},"# Page name like \"03_Riverside\" — zero-padded index plus district name\n",[286,717,718],{"class":206,"line":305},[286,719,720],{"class":295},"atlas.setPageNameExpression(\n",[286,722,723,726,729,732,734],{"class":206,"line":312},[286,724,725],{"class":335},"    \"lpad(@atlas_featurenumber, 2, '0') || '_' || ",[286,727,728],{"class":354},"\\\"",[286,730,731],{"class":335},"district_name",[286,733,728],{"class":354},[286,735,736],{"class":335},"\"\n",[286,738,739],{"class":206,"line":324},[286,740,339],{"class":295},[14,742,743,474,745,748,749,752,753,756,757,760,761,764,765,769,770,35],{},[48,744,473],{},[27,746,747],{},"setPageNameExpression(...)"," is evaluated once per feature. Here ",[27,750,751],{},"@atlas_featurenumber"," is the 1-based position in the (filtered, sorted) series, ",[27,754,755],{},"lpad(...)"," zero-pads it so files sort correctly, and ",[27,758,759],{},"\"district_name\""," pulls the field value. When you export \"one file per feature\", this expression becomes each filename. Layout text items can use the same variables — ",[27,762,763],{},"[% \"district_name\" %]"," in a label updates automatically as the atlas iterates. For dynamic ",[766,767,768],"em",{},"styling"," per page (for example a choropleth that re-classifies per district), pair this with the renderer techniques in ",[18,771,773],{"href":772},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","creating a choropleth map in PyQGIS",[37,775,777],{"id":776},"controlling-per-page-extent","Controlling Per-Page Extent",[14,779,780,781,35],{},"The map item flagged \"controlled by atlas\" reframes to each coverage feature. You govern the framing with margin or scale settings on that ",[27,782,783],{},"QgsLayoutItemMap",[277,785,787],{"className":279,"code":786,"language":281,"meta":282,"style":282},"from qgis.core import QgsLayoutItemMap\n\n# Find the atlas-controlled map item in the layout\nmap_item = next(\n    item for item in layout.items()\n    if isinstance(item, QgsLayoutItemMap) and item.atlasDriven()\n)\n\n# Frame each feature with a 15% margin around its geometry\nmap_item.setAtlasScalingMode(QgsLayoutItemMap.AtlasScalingMode.Auto)\nmap_item.setAtlasMargin(0.15)\n",[27,788,789,800,804,809,822,839,856,860,864,869,874],{"__ignoreMap":282},[286,790,791,793,795,797],{"class":206,"line":288},[286,792,292],{"class":291},[286,794,296],{"class":295},[286,796,299],{"class":291},[286,798,799],{"class":295}," QgsLayoutItemMap\n",[286,801,802],{"class":206,"line":305},[286,803,309],{"emptyLinePlaceholder":308},[286,805,806],{"class":206,"line":312},[286,807,808],{"class":524},"# Find the atlas-controlled map item in the layout\n",[286,810,811,814,816,819],{"class":206,"line":324},[286,812,813],{"class":295},"map_item ",[286,815,318],{"class":291},[286,817,818],{"class":354}," next",[286,820,821],{"class":295},"(\n",[286,823,824,827,830,833,836],{"class":206,"line":342},[286,825,826],{"class":295},"    item ",[286,828,829],{"class":291},"for",[286,831,832],{"class":295}," item ",[286,834,835],{"class":291},"in",[286,837,838],{"class":295}," layout.items()\n",[286,840,841,844,847,850,853],{"class":206,"line":361},[286,842,843],{"class":291},"    if",[286,845,846],{"class":354}," isinstance",[286,848,849],{"class":295},"(item, QgsLayoutItemMap) ",[286,851,852],{"class":291},"and",[286,854,855],{"class":295}," item.atlasDriven()\n",[286,857,858],{"class":206,"line":378},[286,859,339],{"class":295},[286,861,862],{"class":206,"line":383},[286,863,309],{"emptyLinePlaceholder":308},[286,865,866],{"class":206,"line":394},[286,867,868],{"class":524},"# Frame each feature with a 15% margin around its geometry\n",[286,870,871],{"class":206,"line":405},[286,872,873],{"class":295},"map_item.setAtlasScalingMode(QgsLayoutItemMap.AtlasScalingMode.Auto)\n",[286,875,876,879,882],{"class":206,"line":410},[286,877,878],{"class":295},"map_item.setAtlasMargin(",[286,880,881],{"class":354},"0.15",[286,883,339],{"class":295},[14,885,886,474,888,891,892,894,895,898,899,901,902,905,906,909,910,913,914,917],{},[48,887,473],{},[27,889,890],{},"layout.items()"," returns every layout element; filtering for a ",[27,893,783],{}," whose ",[27,896,897],{},"atlasDriven()"," is ",[27,900,400],{}," finds the map that follows the atlas. ",[27,903,904],{},"AtlasScalingMode.Auto"," recomputes the scale per feature so the geometry fits; ",[27,907,908],{},"setAtlasMargin(0.15)"," adds 15% padding so features are not flush against the frame. The alternatives are ",[27,911,912],{},"Fixed"," (constant scale, frame centres on each feature) and ",[27,915,916],{},"Predefined"," (snaps to a configured scale list) — pick based on whether consistent scale or consistent framing matters more for your map book.",[37,919,921],{"id":920},"exporting-the-whole-series","Exporting the Whole Series",[14,923,924,925,928,929,932,933,936],{},"With the atlas configured, ",[27,926,927],{},"QgsLayoutExporter"," exports the series. ",[27,930,931],{},"exportToPdf"," (atlas overload) can produce one combined PDF or one PDF per feature; ",[27,934,935],{},"exportToImage"," writes a folder of rasters.",[277,938,940],{"className":279,"code":939,"language":281,"meta":282,"style":282},"import os\nfrom qgis.core import QgsLayoutExporter\n\nout_dir = \"\u002Fdata\u002Fatlas_out\"\nos.makedirs(out_dir, exist_ok=True)\nexporter = QgsLayoutExporter(layout)\n\npdf_settings = QgsLayoutExporter.PdfExportSettings()\npdf_settings.dpi = 300\npdf_settings.forceVectorOutput = True\n\n# A) One combined multi-page PDF\nresult, error = exporter.exportToPdf(\n    atlas, os.path.join(out_dir, \"district_atlas.pdf\"), pdf_settings\n)\n\n# B) One PDF per feature (filenames from pageNameExpression)\n# result, error = exporter.exportToPdfs(\n#     atlas, os.path.join(out_dir, \"districts\"), pdf_settings\n# )\n\n# C) One PNG per feature\n# img_settings = QgsLayoutExporter.ImageExportSettings()\n# img_settings.dpi = 200\n# result, error = exporter.exportToImage(\n#     atlas, os.path.join(out_dir, \"districts\"), \"png\", img_settings\n# )\n\nif result == QgsLayoutExporter.Success:\n    print(\"Atlas exported successfully\")\nelse:\n    print(\"Export failed:\", error)\n",[27,941,942,949,960,964,974,989,999,1003,1013,1023,1033,1037,1042,1052,1063,1068,1073,1079,1085,1091,1097,1102,1108,1114,1120,1126,1132,1137,1142,1156,1168,1176],{"__ignoreMap":282},[286,943,944,946],{"class":206,"line":288},[286,945,299],{"class":291},[286,947,948],{"class":295}," os\n",[286,950,951,953,955,957],{"class":206,"line":305},[286,952,292],{"class":291},[286,954,296],{"class":295},[286,956,299],{"class":291},[286,958,959],{"class":295}," QgsLayoutExporter\n",[286,961,962],{"class":206,"line":312},[286,963,309],{"emptyLinePlaceholder":308},[286,965,966,969,971],{"class":206,"line":324},[286,967,968],{"class":295},"out_dir ",[286,970,318],{"class":291},[286,972,973],{"class":335}," \"\u002Fdata\u002Fatlas_out\"\n",[286,975,976,979,983,985,987],{"class":206,"line":342},[286,977,978],{"class":295},"os.makedirs(out_dir, ",[286,980,982],{"class":981},"s9osk","exist_ok",[286,984,318],{"class":291},[286,986,400],{"class":354},[286,988,339],{"class":295},[286,990,991,994,996],{"class":206,"line":361},[286,992,993],{"class":295},"exporter ",[286,995,318],{"class":291},[286,997,998],{"class":295}," QgsLayoutExporter(layout)\n",[286,1000,1001],{"class":206,"line":378},[286,1002,309],{"emptyLinePlaceholder":308},[286,1004,1005,1008,1010],{"class":206,"line":383},[286,1006,1007],{"class":295},"pdf_settings ",[286,1009,318],{"class":291},[286,1011,1012],{"class":295}," QgsLayoutExporter.PdfExportSettings()\n",[286,1014,1015,1018,1020],{"class":206,"line":394},[286,1016,1017],{"class":295},"pdf_settings.dpi ",[286,1019,318],{"class":291},[286,1021,1022],{"class":354}," 300\n",[286,1024,1025,1028,1030],{"class":206,"line":405},[286,1026,1027],{"class":295},"pdf_settings.forceVectorOutput ",[286,1029,318],{"class":291},[286,1031,1032],{"class":354}," True\n",[286,1034,1035],{"class":206,"line":410},[286,1036,309],{"emptyLinePlaceholder":308},[286,1038,1039],{"class":206,"line":432},[286,1040,1041],{"class":524},"# A) One combined multi-page PDF\n",[286,1043,1044,1047,1049],{"class":206,"line":438},[286,1045,1046],{"class":295},"result, error ",[286,1048,318],{"class":291},[286,1050,1051],{"class":295}," exporter.exportToPdf(\n",[286,1053,1054,1057,1060],{"class":206,"line":443},[286,1055,1056],{"class":295},"    atlas, os.path.join(out_dir, ",[286,1058,1059],{"class":335},"\"district_atlas.pdf\"",[286,1061,1062],{"class":295},"), pdf_settings\n",[286,1064,1066],{"class":206,"line":1065},15,[286,1067,339],{"class":295},[286,1069,1071],{"class":206,"line":1070},16,[286,1072,309],{"emptyLinePlaceholder":308},[286,1074,1076],{"class":206,"line":1075},17,[286,1077,1078],{"class":524},"# B) One PDF per feature (filenames from pageNameExpression)\n",[286,1080,1082],{"class":206,"line":1081},18,[286,1083,1084],{"class":524},"# result, error = exporter.exportToPdfs(\n",[286,1086,1088],{"class":206,"line":1087},19,[286,1089,1090],{"class":524},"#     atlas, os.path.join(out_dir, \"districts\"), pdf_settings\n",[286,1092,1094],{"class":206,"line":1093},20,[286,1095,1096],{"class":524},"# )\n",[286,1098,1100],{"class":206,"line":1099},21,[286,1101,309],{"emptyLinePlaceholder":308},[286,1103,1105],{"class":206,"line":1104},22,[286,1106,1107],{"class":524},"# C) One PNG per feature\n",[286,1109,1111],{"class":206,"line":1110},23,[286,1112,1113],{"class":524},"# img_settings = QgsLayoutExporter.ImageExportSettings()\n",[286,1115,1117],{"class":206,"line":1116},24,[286,1118,1119],{"class":524},"# img_settings.dpi = 200\n",[286,1121,1123],{"class":206,"line":1122},25,[286,1124,1125],{"class":524},"# result, error = exporter.exportToImage(\n",[286,1127,1129],{"class":206,"line":1128},26,[286,1130,1131],{"class":524},"#     atlas, os.path.join(out_dir, \"districts\"), \"png\", img_settings\n",[286,1133,1135],{"class":206,"line":1134},27,[286,1136,1096],{"class":524},[286,1138,1140],{"class":206,"line":1139},28,[286,1141,309],{"emptyLinePlaceholder":308},[286,1143,1145,1147,1150,1153],{"class":206,"line":1144},29,[286,1146,345],{"class":291},[286,1148,1149],{"class":295}," result ",[286,1151,1152],{"class":291},"==",[286,1154,1155],{"class":295}," QgsLayoutExporter.Success:\n",[286,1157,1159,1161,1163,1166],{"class":206,"line":1158},30,[286,1160,583],{"class":354},[286,1162,370],{"class":295},[286,1164,1165],{"class":335},"\"Atlas exported successfully\"",[286,1167,339],{"class":295},[286,1169,1171,1174],{"class":206,"line":1170},31,[286,1172,1173],{"class":291},"else",[286,1175,358],{"class":295},[286,1177,1179,1181,1183,1186],{"class":206,"line":1178},32,[286,1180,583],{"class":354},[286,1182,370],{"class":295},[286,1184,1185],{"class":335},"\"Export failed:\"",[286,1187,1188],{"class":295},", error)\n",[14,1190,1191,1193,1194,1197,1198,1201,1202,1205,1206,1208,1209,1212,1213,1216,1217,1219,1220,1223,1224,1226,1227,1231],{},[48,1192,473],{}," The atlas-aware overloads take the ",[27,1195,1196],{},"atlas"," object as their first argument, which is what makes them iterate instead of rendering a single page. ",[27,1199,1200],{},"exportToPdf(atlas, ...)"," concatenates all pages into one document; ",[27,1203,1204],{},"exportToPdfs(atlas, base, ...)"," writes a separate file per feature named from the ",[27,1207,700],{},". ",[27,1210,1211],{},"exportToImage(atlas, base, \"png\", ...)"," does the same for rasters. The atlas overloads return a ",[27,1214,1215],{},"(result, error)"," tuple — unlike the single-layout ",[27,1218,931],{},", which returns only a code — so unpack two values and check ",[27,1221,1222],{},"result == QgsLayoutExporter.Success",". For a complete standalone script around option A, see ",[18,1225,34],{"href":33},". The same exporter underpins ",[18,1228,1230],{"href":1229},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fexporting-multiple-qgis-layouts-to-pdf\u002F","exporting multiple QGIS layouts to PDF",", the non-atlas case.",[37,1233,1235],{"id":1234},"building-a-layout-and-atlas-entirely-from-code","Building a Layout and Atlas Entirely from Code",[14,1237,1238,1239,1241],{},"Most workflows start from a layout designed in the GUI, but you can construct the whole layout and its atlas programmatically — useful for templated map books generated on demand. The key is creating a ",[27,1240,783],{},", flagging it atlas-driven, then wiring the atlas to it.",[277,1243,1245],{"className":279,"code":1244,"language":281,"meta":282,"style":282},"from qgis.core import (\n    QgsProject, QgsPrintLayout, QgsLayoutItemMap,\n    QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes,\n)\n\nproject = QgsProject.instance()\ncoverage = project.mapLayersByName(\"districts\")[0]\n\nlayout = QgsPrintLayout(project)\nlayout.initializeDefaults()\nlayout.setName(\"GeneratedAtlas\")\nproject.layoutManager().addLayout(layout)\n\n# Atlas-controlled map item\nmap_item = QgsLayoutItemMap(layout)\nmap_item.attemptMove(QgsLayoutPoint(10, 10, QgsUnitTypes.LayoutMillimeters))\nmap_item.attemptResize(QgsLayoutSize(190, 250, QgsUnitTypes.LayoutMillimeters))\nmap_item.setLayers(list(project.mapLayers().values()))\nmap_item.setAtlasDriven(True)\nmap_item.setAtlasScalingMode(QgsLayoutItemMap.AtlasScalingMode.Auto)\nmap_item.setAtlasMargin(0.1)\nlayout.addLayoutItem(map_item)\n\n# Wire the atlas\natlas = layout.atlas()\natlas.setEnabled(True)\natlas.setCoverageLayer(coverage)\natlas.updateFeatures()\nprint(f\"Generated atlas with {atlas.count()} pages\")\n",[27,1246,1247,1258,1263,1268,1272,1276,1284,1300,1304,1313,1318,1328,1333,1337,1342,1351,1367,1381,1392,1401,1405,1414,1419,1423,1428,1436,1444,1448,1453],{"__ignoreMap":282},[286,1248,1249,1251,1253,1255],{"class":206,"line":288},[286,1250,292],{"class":291},[286,1252,296],{"class":295},[286,1254,299],{"class":291},[286,1256,1257],{"class":295}," (\n",[286,1259,1260],{"class":206,"line":305},[286,1261,1262],{"class":295},"    QgsProject, QgsPrintLayout, QgsLayoutItemMap,\n",[286,1264,1265],{"class":206,"line":312},[286,1266,1267],{"class":295},"    QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes,\n",[286,1269,1270],{"class":206,"line":324},[286,1271,339],{"class":295},[286,1273,1274],{"class":206,"line":342},[286,1275,309],{"emptyLinePlaceholder":308},[286,1277,1278,1280,1282],{"class":206,"line":361},[286,1279,315],{"class":295},[286,1281,318],{"class":291},[286,1283,321],{"class":295},[286,1285,1286,1288,1290,1292,1294,1296,1298],{"class":206,"line":378},[286,1287,413],{"class":295},[286,1289,318],{"class":291},[286,1291,418],{"class":295},[286,1293,421],{"class":335},[286,1295,424],{"class":295},[286,1297,112],{"class":354},[286,1299,429],{"class":295},[286,1301,1302],{"class":206,"line":383},[286,1303,309],{"emptyLinePlaceholder":308},[286,1305,1306,1308,1310],{"class":206,"line":394},[286,1307,327],{"class":295},[286,1309,318],{"class":291},[286,1311,1312],{"class":295}," QgsPrintLayout(project)\n",[286,1314,1315],{"class":206,"line":405},[286,1316,1317],{"class":295},"layout.initializeDefaults()\n",[286,1319,1320,1323,1326],{"class":206,"line":410},[286,1321,1322],{"class":295},"layout.setName(",[286,1324,1325],{"class":335},"\"GeneratedAtlas\"",[286,1327,339],{"class":295},[286,1329,1330],{"class":206,"line":432},[286,1331,1332],{"class":295},"project.layoutManager().addLayout(layout)\n",[286,1334,1335],{"class":206,"line":438},[286,1336,309],{"emptyLinePlaceholder":308},[286,1338,1339],{"class":206,"line":443},[286,1340,1341],{"class":524},"# Atlas-controlled map item\n",[286,1343,1344,1346,1348],{"class":206,"line":1065},[286,1345,813],{"class":295},[286,1347,318],{"class":291},[286,1349,1350],{"class":295}," QgsLayoutItemMap(layout)\n",[286,1352,1353,1356,1359,1362,1364],{"class":206,"line":1070},[286,1354,1355],{"class":295},"map_item.attemptMove(QgsLayoutPoint(",[286,1357,1358],{"class":354},"10",[286,1360,1361],{"class":295},", ",[286,1363,1358],{"class":354},[286,1365,1366],{"class":295},", QgsUnitTypes.LayoutMillimeters))\n",[286,1368,1369,1372,1375,1377,1379],{"class":206,"line":1075},[286,1370,1371],{"class":295},"map_item.attemptResize(QgsLayoutSize(",[286,1373,1374],{"class":354},"190",[286,1376,1361],{"class":295},[286,1378,223],{"class":354},[286,1380,1366],{"class":295},[286,1382,1383,1386,1389],{"class":206,"line":1081},[286,1384,1385],{"class":295},"map_item.setLayers(",[286,1387,1388],{"class":354},"list",[286,1390,1391],{"class":295},"(project.mapLayers().values()))\n",[286,1393,1394,1397,1399],{"class":206,"line":1087},[286,1395,1396],{"class":295},"map_item.setAtlasDriven(",[286,1398,400],{"class":354},[286,1400,339],{"class":295},[286,1402,1403],{"class":206,"line":1093},[286,1404,873],{"class":295},[286,1406,1407,1409,1412],{"class":206,"line":1099},[286,1408,878],{"class":295},[286,1410,1411],{"class":354},"0.1",[286,1413,339],{"class":295},[286,1415,1416],{"class":206,"line":1104},[286,1417,1418],{"class":295},"layout.addLayoutItem(map_item)\n",[286,1420,1421],{"class":206,"line":1110},[286,1422,309],{"emptyLinePlaceholder":308},[286,1424,1425],{"class":206,"line":1116},[286,1426,1427],{"class":524},"# Wire the atlas\n",[286,1429,1430,1432,1434],{"class":206,"line":1122},[286,1431,386],{"class":295},[286,1433,318],{"class":291},[286,1435,391],{"class":295},[286,1437,1438,1440,1442],{"class":206,"line":1128},[286,1439,397],{"class":295},[286,1441,400],{"class":354},[286,1443,339],{"class":295},[286,1445,1446],{"class":206,"line":1134},[286,1447,435],{"class":295},[286,1449,1450],{"class":206,"line":1139},[286,1451,1452],{"class":295},"atlas.updateFeatures()\n",[286,1454,1455,1457,1459,1461,1464,1466,1468,1470,1472],{"class":206,"line":1144},[286,1456,446],{"class":354},[286,1458,370],{"class":295},[286,1460,451],{"class":291},[286,1462,1463],{"class":335},"\"Generated atlas with ",[286,1465,457],{"class":354},[286,1467,651],{"class":295},[286,1469,463],{"class":354},[286,1471,656],{"class":335},[286,1473,339],{"class":295},[14,1475,1476,474,1478,510,1481,1484,1485,1488,1489,1491,1492,1495,1496,1499,1500,1503,1504,1506],{},[48,1477,473],{},[27,1479,1480],{},"QgsPrintLayout(project)",[27,1482,1483],{},"initializeDefaults()"," creates a blank A4 layout; ",[27,1486,1487],{},"addLayout"," registers it so the layout manager (and later export) can find it. The ",[27,1490,783],{}," is positioned and sized in millimetres, then ",[27,1493,1494],{},"setAtlasDriven(True)"," is the switch that makes this map follow the atlas — without it the atlas iterates but the map never reframes. ",[27,1497,1498],{},"setAtlasScalingMode"," and ",[27,1501,1502],{},"setAtlasMargin"," govern per-page framing exactly as on a GUI-built layout. This mirrors the single-layout construction in ",[18,1505,84],{"href":83},", adding the atlas wiring on top.",[37,1508,1510],{"id":1509},"compatibility-notes","Compatibility Notes",[1512,1513,1514,1527],"table",{},[1515,1516,1517],"thead",{},[1518,1519,1520,1524],"tr",{},[1521,1522,1523],"th",{},"QGIS \u002F Python",[1521,1525,1526],{},"Atlas API notes",[1528,1529,1530,1549,1564],"tbody",{},[1518,1531,1532,1536],{},[1533,1534,1535],"td",{},"3.28 LTR \u002F Py 3.9",[1533,1537,1538,1540,1541,1544,1545,1548],{},[27,1539,270],{}," API stable. Enum access via flat names (e.g. ",[27,1542,1543],{},"QgsLayoutItemMap.Auto",") works. ",[27,1546,1547],{},"setFilterExpression"," may not return a bool.",[1518,1550,1551,1556],{},[1533,1552,1553],{},[48,1554,1555],{},"3.34 LTR \u002F Py 3.12 (baseline)",[1533,1557,1558,1559,1561,1562,35],{},"Recommended. Scoped enums (",[27,1560,904],{},") available; atlas export overloads return ",[27,1563,1215],{},[1518,1565,1566,1569],{},[1533,1567,1568],{},"3.40 \u002F 3.44 \u002F Py 3.12",[1533,1570,1571],{},"Same model. Minor expression-function additions; re-verify any expression-driven settings.",[14,1573,1574,1575,1578,1579,1581],{},"Across versions, always call ",[27,1576,1577],{},"atlas.updateFeatures()"," after changing the coverage layer, filter, or sort, and always verify ",[27,1580,651],{}," before a long export so you do not render an empty or unexpectedly large series.",[37,1583,1585],{"id":1584},"key-takeaways","Key Takeaways",[42,1587,1588,1600,1609,1617,1627],{},[45,1589,1590,1591,1593,1594,1596,1597,1599],{},"An atlas is a ",[27,1592,270],{}," owned by a ",[27,1595,266],{},"; reach it via ",[27,1598,274],{},", enable it, and bind a coverage layer.",[45,1601,1602,1603,1605,1606,1608],{},"Filtering and sorting shape the series — call ",[27,1604,682],{}," afterward and check ",[27,1607,651],{}," to confirm the page list.",[45,1610,1611,1613,1614,1616],{},[27,1612,700],{}," (with ",[27,1615,751],{}," and field references) names per-feature outputs and feeds dynamic labels.",[45,1618,1619,1620,1622,1623,1499,1625,35],{},"Per-page framing is controlled on the atlas-driven ",[27,1621,783],{}," via ",[27,1624,1498],{},[27,1626,1502],{},[45,1628,1629,1630,1632,1633,1361,1635,1361,1638,1640,1641,1643],{},"The atlas overloads of ",[27,1631,927],{}," (",[27,1634,931],{},[27,1636,1637],{},"exportToPdfs",[27,1639,935],{},") iterate the series and return a ",[27,1642,1215],{}," tuple.",[37,1645,1647],{"id":1646},"frequently-asked-questions","Frequently Asked Questions",[14,1649,1650,1653,1654,1656,1657,1659,1660,1662,1663,1665,1666,35],{},[48,1651,1652],{},"How do I get the atlas object from a layout?","\nCall ",[27,1655,274],{}," on a ",[27,1658,266],{},". Each print layout owns exactly one ",[27,1661,270],{},". Enable it with ",[27,1664,491],{}," and bind the coverage layer with ",[27,1667,1668],{},"setCoverageLayer(...)",[14,1670,1671,1674,1675,1677,1678,1681,1682,1684],{},[48,1672,1673],{},"Why does my atlas export zero pages?","\nUsually an invalid or over-restrictive filter, or you forgot ",[27,1676,682],{}," after changing the filter. Check ",[27,1679,1680],{},"atlas.filterExpression()"," is valid and inspect ",[27,1683,651],{}," before exporting. A geographic mismatch between coverage and map CRS can also exclude everything.",[14,1686,1687,1695,1698,1699,1702,1703,1705],{},[48,1688,1689,1690,1499,1692,1694],{},"What is the difference between ",[27,1691,931],{},[27,1693,1637],{},"?",[27,1696,1697],{},"exportToPdf(atlas, path, settings)"," writes one combined multi-page PDF. ",[27,1700,1701],{},"exportToPdfs(atlas, base, settings)"," writes one PDF per feature, named from ",[27,1704,700],{},". Both take the atlas object so they iterate the series.",[14,1707,1708,1711,1712,1714,1715,1717],{},[48,1709,1710],{},"How do I make titles and labels change per page?","\nUse expression-based layout labels referencing coverage fields, e.g. ",[27,1713,763],{},", plus atlas variables like ",[27,1716,751],{},". The atlas re-evaluates them for each feature during iteration.",[14,1719,1720,1723,1724,1727,1728,1731,1732,1735],{},[48,1721,1722],{},"Can I run an atlas export headlessly on a server?","\nYes. Bootstrap ",[27,1725,1726],{},"QgsApplication",", read the project with ",[27,1729,1730],{},"QgsProject.instance().read(...)",", configure the atlas, and call the atlas exporter overloads. The dedicated ",[18,1733,1734],{"href":33},"Generate an Atlas PDF in PyQGIS"," guide shows the full standalone script.",[37,1737,1739],{"id":1738},"related","Related",[42,1741,1742,1746,1750,1756],{},[45,1743,1744],{},[18,1745,21],{"href":20},[45,1747,1748],{},[18,1749,84],{"href":83},[45,1751,1752],{},[18,1753,1755],{"href":1754},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","Chaining Processing Algorithms in PyQGIS",[45,1757,1758],{},[18,1759,1734],{"href":33},[1761,1762,1763],"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":282,"searchDepth":305,"depth":305,"links":1765},[1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777],{"id":39,"depth":305,"text":40},{"id":88,"depth":305,"text":89},{"id":259,"depth":305,"text":260},{"id":502,"depth":305,"text":503},{"id":693,"depth":305,"text":694},{"id":776,"depth":305,"text":777},{"id":920,"depth":305,"text":921},{"id":1234,"depth":305,"text":1235},{"id":1509,"depth":305,"text":1510},{"id":1584,"depth":305,"text":1585},{"id":1646,"depth":305,"text":1647},{"id":1738,"depth":305,"text":1739},"Drive a QGIS atlas from code with PyQGIS. Configure coverage layers, filters, sorting, per-page extent, and export the whole series to PDF or images.","md",{},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series",{"title":5,"description":1778},"spatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Findex","iMqYAZKPoSHiAWvCCS67AxCzLDjLm4dYxyJiwYGVX6M",1781792483476]