[{"data":1,"prerenderedAt":2486},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series":3},{"id":4,"title":5,"body":6,"description":2474,"extension":2475,"meta":2476,"navigation":309,"path":2482,"seo":2483,"stem":2484,"__hash__":2485},"docs\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Findex.md","Automating Atlas Map Series in PyQGIS",{"type":7,"value":8,"toc":2457},"minimark",[9,13,23,36,41,86,90,93,258,262,277,470,501,505,516,660,692,696,707,742,775,779,785,885,919,923,926,1066,1113,1117,1130,1258,1290,1294,1308,1436,1688,1731,1735,1741,1971,2004,2008,2068,2078,2082,2085,2166,2173,2177,2254,2258,2279,2295,2316,2328,2346,2374,2392,2398,2404,2410,2414,2453],[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,134,100,140,100,147,100,153,100,157,100,160,100,164,100,168,100,171,100,175,100,179,100,184,100,189,100,192,100,197,100,202,100,206,100,213,100,217,100,221,100,227,100,232,100,238,100,242],"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,133],"text",{"x":128,"y":129,"fill":130,"style":131,"textAnchor":132},"95","155","#17211d","text-anchor:middle;font-family:sans-serif;font-size:13px;font-weight:bold","middle","Coverage layer",[126,135,139],{"x":128,"y":136,"fill":137,"style":138,"textAnchor":132},"173","#2f3b35","text-anchor:middle;font-family:sans-serif;font-size:11px","filter + sort",[110,141],{"x":142,"y":143,"width":144,"height":145,"rx":121,"fill":122,"stroke":146,"style":124},"210","40","140","56","#2563eb",[126,148,152],{"x":149,"y":150,"fill":130,"style":151,"textAnchor":132},"280","65","text-anchor:middle;font-family:sans-serif;font-size:12px;font-weight:bold","Next feature",[126,154,156],{"x":149,"y":155,"fill":137,"style":138,"textAnchor":132},"82","set map extent",[110,158],{"x":142,"y":159,"width":144,"height":145,"rx":121,"fill":122,"stroke":146,"style":124},"135",[126,161,163],{"x":149,"y":162,"fill":130,"style":151,"textAnchor":132},"158","Evaluate",[126,165,167],{"x":149,"y":166,"fill":137,"style":138,"textAnchor":132},"175","page name \u002F labels",[110,169],{"x":142,"y":170,"width":144,"height":145,"rx":121,"fill":122,"stroke":146,"style":124},"230",[126,172,174],{"x":149,"y":173,"fill":130,"style":151,"textAnchor":132},"253","Render layout",[126,176,178],{"x":149,"y":177,"fill":137,"style":138,"textAnchor":132},"270","one page",[110,180],{"x":181,"y":159,"width":144,"height":145,"rx":121,"fill":182,"stroke":183,"style":124},"430","#26322d","#15803d",[126,185,188],{"x":186,"y":162,"fill":187,"style":151,"textAnchor":132},"500","#d9f99d","Export page",[126,190,191],{"x":186,"y":166,"fill":187,"style":138,"textAnchor":132},"PDF \u002F image",[110,193],{"x":194,"y":159,"width":195,"height":145,"rx":121,"fill":122,"stroke":196,"style":124},"620","80","#b45309",[126,198,201],{"x":199,"y":200,"fill":130,"style":151,"textAnchor":132},"660","160","More?",[126,203,205],{"x":199,"y":204,"fill":137,"style":138,"textAnchor":132},"177","yes \u002F no",[207,208],"line",{"x1":200,"y1":209,"x2":210,"y2":211,"stroke":137,"style":212},"150","208","68","stroke-width:2.5;marker-end:url(#b)",[207,214],{"x1":149,"y1":215,"x2":149,"y2":216,"stroke":137,"style":212},"96","133",[207,218],{"x1":149,"y1":219,"x2":149,"y2":220,"stroke":137,"style":212},"191","228",[207,222],{"x1":223,"y1":224,"x2":225,"y2":226,"stroke":137,"style":212},"350","250","428","180",[207,228],{"x1":229,"y1":230,"x2":231,"y2":230,"stroke":137,"style":212},"570","163","618",[233,234],"path",{"d":235,"fill":236,"stroke":196,"style":237},"M660 135 L660 60 L352 60","none","stroke-width:2.5;stroke-dasharray:6 4;marker-end:url(#b)",[126,239,241],{"x":186,"y":240,"fill":196,"style":138,"textAnchor":132},"50","loop: yes",[243,244,245,246,100],"defs",{},"\n    ",[247,248,254,255,245],"marker",{"id":249,"markerWidth":250,"markerHeight":250,"refX":251,"refY":252,"orient":253},"b","9","7","4","auto","\n      ",[233,256],{"d":257,"fill":137},"M0,0 L8,4 L0,8 Z",[37,259,261],{"id":260},"accessing-the-atlas-object","Accessing the Atlas Object",[14,263,264,265,268,269,272,273,276],{},"Every ",[27,266,267],{},"QgsPrintLayout"," owns a ",[27,270,271],{},"QgsLayoutAtlas",", reachable with ",[27,274,275],{},"layout.atlas()",". From there you enable it and bind a coverage layer. This is the foundation every later configuration step builds on.",[278,279,284],"pre",{"className":280,"code":281,"language":282,"meta":283,"style":283},"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,285,286,304,311,323,341,360,377,382,393,404,409,431,437,442],{"__ignoreMap":283},[287,288,290,294,298,301],"span",{"class":207,"line":289},1,[287,291,293],{"class":292},"snl16","from",[287,295,297],{"class":296},"s95oV"," qgis.core ",[287,299,300],{"class":292},"import",[287,302,303],{"class":296}," QgsProject\n",[287,305,307],{"class":207,"line":306},2,[287,308,310],{"emptyLinePlaceholder":309},true,"\n",[287,312,314,317,320],{"class":207,"line":313},3,[287,315,316],{"class":296},"project ",[287,318,319],{"class":292},"=",[287,321,322],{"class":296}," QgsProject.instance()\n",[287,324,326,329,331,334,338],{"class":207,"line":325},4,[287,327,328],{"class":296},"layout ",[287,330,319],{"class":292},[287,332,333],{"class":296}," project.layoutManager().layoutByName(",[287,335,337],{"class":336},"sU2Wk","\"DistrictMaps\"",[287,339,340],{"class":296},")\n",[287,342,344,347,350,353,357],{"class":207,"line":343},5,[287,345,346],{"class":292},"if",[287,348,349],{"class":296}," layout ",[287,351,352],{"class":292},"is",[287,354,356],{"class":355},"sDLfK"," None",[287,358,359],{"class":296},":\n",[287,361,363,366,369,372,375],{"class":207,"line":362},6,[287,364,365],{"class":292},"    raise",[287,367,368],{"class":355}," ValueError",[287,370,371],{"class":296},"(",[287,373,374],{"class":336},"\"Layout 'DistrictMaps' not found in project\"",[287,376,340],{"class":296},[287,378,380],{"class":207,"line":379},7,[287,381,310],{"emptyLinePlaceholder":309},[287,383,385,388,390],{"class":207,"line":384},8,[287,386,387],{"class":296},"atlas ",[287,389,319],{"class":292},[287,391,392],{"class":296}," layout.atlas()\n",[287,394,396,399,402],{"class":207,"line":395},9,[287,397,398],{"class":296},"atlas.setEnabled(",[287,400,401],{"class":355},"True",[287,403,340],{"class":296},[287,405,407],{"class":207,"line":406},10,[287,408,310],{"emptyLinePlaceholder":309},[287,410,412,415,417,420,423,426,428],{"class":207,"line":411},11,[287,413,414],{"class":296},"coverage ",[287,416,319],{"class":292},[287,418,419],{"class":296}," project.mapLayersByName(",[287,421,422],{"class":336},"\"districts\"",[287,424,425],{"class":296},")[",[287,427,112],{"class":355},[287,429,430],{"class":296},"]\n",[287,432,434],{"class":207,"line":433},12,[287,435,436],{"class":296},"atlas.setCoverageLayer(coverage)\n",[287,438,440],{"class":207,"line":439},13,[287,441,310],{"emptyLinePlaceholder":309},[287,443,445,448,450,453,456,459,462,465,468],{"class":207,"line":444},14,[287,446,447],{"class":355},"print",[287,449,371],{"class":296},[287,451,452],{"class":292},"f",[287,454,455],{"class":336},"\"Atlas enabled with ",[287,457,458],{"class":355},"{",[287,460,461],{"class":296},"coverage.featureCount()",[287,463,464],{"class":355},"}",[287,466,467],{"class":336}," candidate pages\"",[287,469,340],{"class":296},[14,471,472,475,476,479,480,483,484,486,487,489,490,493,494,497,498,35],{},[48,473,474],{},"Breakdown:"," ",[27,477,478],{},"layoutManager().layoutByName(...)"," returns the named print layout or ",[27,481,482],{},"None",", so the guard matters. ",[27,485,275],{}," returns the layout's single ",[27,488,271],{},"; ",[27,491,492],{},"setEnabled(True)"," activates iteration. ",[27,495,496],{},"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,499,500],{},"mapLayersByName",[37,502,504],{"id":503},"filtering-and-sorting-pages","Filtering and Sorting Pages",[14,506,507,508,511,512,515],{},"You rarely want every feature. ",[27,509,510],{},"setFilterFeatures(True)"," plus ",[27,513,514],{},"setFilterExpression(...)"," restricts the series to features matching a QGIS expression, and the sort settings control page order — essential for a logical map book.",[278,517,519],{"className":280,"code":518,"language":282,"meta":283,"style":283},"# 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,520,521,527,536,562,580,593,597,606,616,625,629,637],{"__ignoreMap":283},[287,522,523],{"class":207,"line":289},[287,524,526],{"class":525},"sjoCn","# Only include districts flagged active, ordered alphabetically by name\n",[287,528,529,532,534],{"class":207,"line":306},[287,530,531],{"class":296},"atlas.setFilterFeatures(",[287,533,401],{"class":355},[287,535,340],{"class":296},[287,537,538,541,543,546,549,552,555,557,560],{"class":207,"line":313},[287,539,540],{"class":296},"ok ",[287,542,319],{"class":292},[287,544,545],{"class":296}," atlas.setFilterExpression(",[287,547,548],{"class":336},"'\"status\" = ",[287,550,551],{"class":355},"\\'",[287,553,554],{"class":336},"active",[287,556,551],{"class":355},[287,558,559],{"class":336}," AND \"population\" > 1000'",[287,561,340],{"class":296},[287,563,564,566,569,572,575,577],{"class":207,"line":325},[287,565,346],{"class":292},[287,567,568],{"class":292}," not",[287,570,571],{"class":296}," atlas.filterExpression() ",[287,573,574],{"class":292},"or",[287,576,568],{"class":292},[287,578,579],{"class":296}," ok:\n",[287,581,582,585,587,590],{"class":207,"line":343},[287,583,584],{"class":355},"    print",[287,586,371],{"class":296},[287,588,589],{"class":336},"\"Filter expression invalid:\"",[287,591,592],{"class":296},", atlas.filterExpression())\n",[287,594,595],{"class":207,"line":362},[287,596,310],{"emptyLinePlaceholder":309},[287,598,599,602,604],{"class":207,"line":379},[287,600,601],{"class":296},"atlas.setSortFeatures(",[287,603,401],{"class":355},[287,605,340],{"class":296},[287,607,608,611,614],{"class":207,"line":384},[287,609,610],{"class":296},"atlas.setSortExpression(",[287,612,613],{"class":336},"'\"district_name\"'",[287,615,340],{"class":296},[287,617,618,621,623],{"class":207,"line":395},[287,619,620],{"class":296},"atlas.setSortAscending(",[287,622,401],{"class":355},[287,624,340],{"class":296},[287,626,627],{"class":207,"line":406},[287,628,310],{"emptyLinePlaceholder":309},[287,630,631,634],{"class":207,"line":411},[287,632,633],{"class":296},"atlas.updateFeatures()   ",[287,635,636],{"class":525},"# recompute the page list after changing filter\u002Fsort\n",[287,638,639,641,643,645,648,650,653,655,658],{"class":207,"line":433},[287,640,447],{"class":355},[287,642,371],{"class":296},[287,644,452],{"class":292},[287,646,647],{"class":336},"\"Series now has ",[287,649,458],{"class":355},[287,651,652],{"class":296},"atlas.count()",[287,654,464],{"class":355},[287,656,657],{"class":336}," pages\"",[287,659,340],{"class":296},[14,661,662,475,664,666,667,669,670,673,674,673,677,680,681,684,685,688,689,691],{},[48,663,474],{},[27,665,510],{}," switches filtering on; ",[27,668,514],{}," 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,671,672],{},"setSortFeatures"," \u002F ",[27,675,676],{},"setSortExpression",[27,678,679],{},"setSortAscending"," define order. Crucially, ",[27,682,683],{},"updateFeatures()"," recomputes the filtered, sorted page list; without it ",[27,686,687],{},"count()"," may report stale numbers. Use ",[27,690,652],{}," to verify you have the pages you expect before exporting.",[37,693,695],{"id":694},"naming-pages-and-driving-dynamic-content","Naming Pages and Driving Dynamic Content",[14,697,698,699,702,703,706],{},"The ",[27,700,701],{},"pageNameExpression"," gives each page a meaningful name, used for per-feature output filenames and accessible to layout labels. Combined with the ",[27,704,705],{},"@atlas_*"," expression variables, this is how titles and stats update per page.",[278,708,710],{"className":280,"code":709,"language":282,"meta":283,"style":283},"# Page name like \"03_Riverside\" — zero-padded index plus district name\natlas.setPageNameExpression(\n    \"lpad(@atlas_featurenumber, 2, '0') || '_' || \\\"district_name\\\"\"\n)\n",[27,711,712,717,722,738],{"__ignoreMap":283},[287,713,714],{"class":207,"line":289},[287,715,716],{"class":525},"# Page name like \"03_Riverside\" — zero-padded index plus district name\n",[287,718,719],{"class":207,"line":306},[287,720,721],{"class":296},"atlas.setPageNameExpression(\n",[287,723,724,727,730,733,735],{"class":207,"line":313},[287,725,726],{"class":336},"    \"lpad(@atlas_featurenumber, 2, '0') || '_' || ",[287,728,729],{"class":355},"\\\"",[287,731,732],{"class":336},"district_name",[287,734,729],{"class":355},[287,736,737],{"class":336},"\"\n",[287,739,740],{"class":207,"line":325},[287,741,340],{"class":296},[14,743,744,475,746,749,750,753,754,757,758,761,762,765,766,770,771,35],{},[48,745,474],{},[27,747,748],{},"setPageNameExpression(...)"," is evaluated once per feature. Here ",[27,751,752],{},"@atlas_featurenumber"," is the 1-based position in the (filtered, sorted) series, ",[27,755,756],{},"lpad(...)"," zero-pads it so files sort correctly, and ",[27,759,760],{},"\"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,763,764],{},"[% \"district_name\" %]"," in a label updates automatically as the atlas iterates. For dynamic ",[767,768,769],"em",{},"styling"," per page (for example a choropleth that re-classifies per district), pair this with the renderer techniques in ",[18,772,774],{"href":773},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","creating a choropleth map in PyQGIS",[37,776,778],{"id":777},"controlling-per-page-extent","Controlling Per-Page Extent",[14,780,781,782,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,783,784],{},"QgsLayoutItemMap",[278,786,788],{"className":280,"code":787,"language":282,"meta":283,"style":283},"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,789,790,801,805,810,823,840,857,861,865,870,875],{"__ignoreMap":283},[287,791,792,794,796,798],{"class":207,"line":289},[287,793,293],{"class":292},[287,795,297],{"class":296},[287,797,300],{"class":292},[287,799,800],{"class":296}," QgsLayoutItemMap\n",[287,802,803],{"class":207,"line":306},[287,804,310],{"emptyLinePlaceholder":309},[287,806,807],{"class":207,"line":313},[287,808,809],{"class":525},"# Find the atlas-controlled map item in the layout\n",[287,811,812,815,817,820],{"class":207,"line":325},[287,813,814],{"class":296},"map_item ",[287,816,319],{"class":292},[287,818,819],{"class":355}," next",[287,821,822],{"class":296},"(\n",[287,824,825,828,831,834,837],{"class":207,"line":343},[287,826,827],{"class":296},"    item ",[287,829,830],{"class":292},"for",[287,832,833],{"class":296}," item ",[287,835,836],{"class":292},"in",[287,838,839],{"class":296}," layout.items()\n",[287,841,842,845,848,851,854],{"class":207,"line":362},[287,843,844],{"class":292},"    if",[287,846,847],{"class":355}," isinstance",[287,849,850],{"class":296},"(item, QgsLayoutItemMap) ",[287,852,853],{"class":292},"and",[287,855,856],{"class":296}," item.atlasDriven()\n",[287,858,859],{"class":207,"line":379},[287,860,340],{"class":296},[287,862,863],{"class":207,"line":384},[287,864,310],{"emptyLinePlaceholder":309},[287,866,867],{"class":207,"line":395},[287,868,869],{"class":525},"# Frame each feature with a 15% margin around its geometry\n",[287,871,872],{"class":207,"line":406},[287,873,874],{"class":296},"map_item.setAtlasScalingMode(QgsLayoutItemMap.AtlasScalingMode.Auto)\n",[287,876,877,880,883],{"class":207,"line":411},[287,878,879],{"class":296},"map_item.setAtlasMargin(",[287,881,882],{"class":355},"0.15",[287,884,340],{"class":296},[14,886,887,475,889,892,893,895,896,899,900,902,903,906,907,910,911,914,915,918],{},[48,888,474],{},[27,890,891],{},"layout.items()"," returns every layout element; filtering for a ",[27,894,784],{}," whose ",[27,897,898],{},"atlasDriven()"," is ",[27,901,401],{}," finds the map that follows the atlas. ",[27,904,905],{},"AtlasScalingMode.Auto"," recomputes the scale per feature so the geometry fits; ",[27,908,909],{},"setAtlasMargin(0.15)"," adds 15% padding so features are not flush against the frame. The alternatives are ",[27,912,913],{},"Fixed"," (constant scale, frame centres on each feature) and ",[27,916,917],{},"Predefined"," (snaps to a configured scale list) — pick based on whether consistent scale or consistent framing matters more for your map book.",[37,920,922],{"id":921},"previewing-and-stepping-through-pages","Previewing and Stepping Through Pages",[14,924,925],{},"Before committing to a long export you often want to inspect a single page — to confirm the extent, the page name, and any expression-driven labels look right. The atlas exposes a small navigation API that seeks to one feature at a time and updates the controlled map, without rendering anything to disk.",[278,927,929],{"className":280,"code":928,"language":282,"meta":283,"style":283},"# Enter preview mode and walk the series feature by feature\natlas.beginRender()          # prepares the atlas for iteration\natlas.first()                # jump to the first page in the filtered\u002Fsorted series\n\nprint(\"Page 1:\", atlas.currentFeatureNumber() + 1, \"\u002F\", atlas.count())\nprint(\"Page name:\", atlas.nameForPage(atlas.currentFeatureNumber()))\n\natlas.next()                 # advance one page; returns False past the end\natlas.seekTo(4)              # jump straight to the 5th page (0-based index)\n\nfeature = atlas.currentFeature()   # the coverage feature backing this page\nprint(\"Now on:\", feature[\"district_name\"])\n\natlas.endRender()            # leave preview mode and restore the layout\n",[27,930,931,936,944,952,956,983,995,999,1007,1020,1024,1037,1054,1058],{"__ignoreMap":283},[287,932,933],{"class":207,"line":289},[287,934,935],{"class":525},"# Enter preview mode and walk the series feature by feature\n",[287,937,938,941],{"class":207,"line":306},[287,939,940],{"class":296},"atlas.beginRender()          ",[287,942,943],{"class":525},"# prepares the atlas for iteration\n",[287,945,946,949],{"class":207,"line":313},[287,947,948],{"class":296},"atlas.first()                ",[287,950,951],{"class":525},"# jump to the first page in the filtered\u002Fsorted series\n",[287,953,954],{"class":207,"line":325},[287,955,310],{"emptyLinePlaceholder":309},[287,957,958,960,962,965,968,971,974,977,980],{"class":207,"line":343},[287,959,447],{"class":355},[287,961,371],{"class":296},[287,963,964],{"class":336},"\"Page 1:\"",[287,966,967],{"class":296},", atlas.currentFeatureNumber() ",[287,969,970],{"class":292},"+",[287,972,973],{"class":355}," 1",[287,975,976],{"class":296},", ",[287,978,979],{"class":336},"\"\u002F\"",[287,981,982],{"class":296},", atlas.count())\n",[287,984,985,987,989,992],{"class":207,"line":362},[287,986,447],{"class":355},[287,988,371],{"class":296},[287,990,991],{"class":336},"\"Page name:\"",[287,993,994],{"class":296},", atlas.nameForPage(atlas.currentFeatureNumber()))\n",[287,996,997],{"class":207,"line":379},[287,998,310],{"emptyLinePlaceholder":309},[287,1000,1001,1004],{"class":207,"line":384},[287,1002,1003],{"class":296},"atlas.next()                 ",[287,1005,1006],{"class":525},"# advance one page; returns False past the end\n",[287,1008,1009,1012,1014,1017],{"class":207,"line":395},[287,1010,1011],{"class":296},"atlas.seekTo(",[287,1013,252],{"class":355},[287,1015,1016],{"class":296},")              ",[287,1018,1019],{"class":525},"# jump straight to the 5th page (0-based index)\n",[287,1021,1022],{"class":207,"line":406},[287,1023,310],{"emptyLinePlaceholder":309},[287,1025,1026,1029,1031,1034],{"class":207,"line":411},[287,1027,1028],{"class":296},"feature ",[287,1030,319],{"class":292},[287,1032,1033],{"class":296}," atlas.currentFeature()   ",[287,1035,1036],{"class":525},"# the coverage feature backing this page\n",[287,1038,1039,1041,1043,1046,1049,1051],{"class":207,"line":433},[287,1040,447],{"class":355},[287,1042,371],{"class":296},[287,1044,1045],{"class":336},"\"Now on:\"",[287,1047,1048],{"class":296},", feature[",[287,1050,760],{"class":336},[287,1052,1053],{"class":296},"])\n",[287,1055,1056],{"class":207,"line":439},[287,1057,310],{"emptyLinePlaceholder":309},[287,1059,1060,1063],{"class":207,"line":444},[287,1061,1062],{"class":296},"atlas.endRender()            ",[287,1064,1065],{"class":525},"# leave preview mode and restore the layout\n",[14,1067,1068,475,1070,1073,1074,976,1077,976,1080,976,1083,1086,1087,1090,1091,1093,1094,1097,1098,1101,1102,1105,1106,1108,1109,1112],{},[48,1069,474],{},[27,1071,1072],{},"beginRender()"," primes the atlas so ",[27,1075,1076],{},"first()",[27,1078,1079],{},"next()",[27,1081,1082],{},"previous()",[27,1084,1085],{},"last()",", and ",[27,1088,1089],{},"seekTo(i)"," can move the current page; each move reframes the atlas-driven ",[27,1092,784],{}," and re-evaluates page expressions exactly as a real export would. ",[27,1095,1096],{},"currentFeatureNumber()"," is the 0-based index into the filtered, sorted series (add 1 for a human-facing page number), and ",[27,1099,1100],{},"currentFeature()"," returns the underlying ",[27,1103,1104],{},"QgsFeature"," so you can assert on its attributes in a test. Always pair ",[27,1107,1072],{}," with ",[27,1110,1111],{},"endRender()"," — leaving the atlas mid-render can strand the layout on the last previewed feature. This navigation API is also how you drive a live map canvas or a Qt preview widget when building an interactive map-book tool.",[37,1114,1116],{"id":1115},"data-defined-output-paths-and-per-page-overrides","Data-Defined Output Paths and Per-Page Overrides",[14,1118,1119,1120,1123,1124,58,1127,35],{},"For a real map book you rarely want a flat folder of files named only by district. The atlas honours the layout's ",[767,1121,1122],{},"data-defined output filename"," expression, so you can bucket pages into subfolders (by region, by status) and stamp the export date into each name — all evaluated per feature during ",[27,1125,1126],{},"exportToPdfs",[27,1128,1129],{},"exportToImage",[278,1131,1133],{"className":280,"code":1132,"language":282,"meta":283,"style":283},"# Group output by region, then name each file by page number + district\natlas.setFilenameExpression(\n    \"'\u002Fdata\u002Fatlas_out\u002F' || \\\"region\\\" || '\u002F' \"\n    \"|| lpad(@atlas_featurenumber, 2, '0') || '_' || \\\"district_name\\\"\"\n)\nif not atlas.filenameExpression():\n    print(\"Filename expression rejected — falling back to page name\")\n\n# Restrict the export to one region without touching the master filter\nregion_atlas_filter = '\"region\" = \\'North\\''\natlas.setFilterExpression(region_atlas_filter)\natlas.updateFeatures()\nprint(f\"Exporting {atlas.count()} North-region pages\")\n",[27,1134,1135,1140,1145,1160,1173,1177,1186,1197,1201,1206,1226,1231,1236],{"__ignoreMap":283},[287,1136,1137],{"class":207,"line":289},[287,1138,1139],{"class":525},"# Group output by region, then name each file by page number + district\n",[287,1141,1142],{"class":207,"line":306},[287,1143,1144],{"class":296},"atlas.setFilenameExpression(\n",[287,1146,1147,1150,1152,1155,1157],{"class":207,"line":313},[287,1148,1149],{"class":336},"    \"'\u002Fdata\u002Fatlas_out\u002F' || ",[287,1151,729],{"class":355},[287,1153,1154],{"class":336},"region",[287,1156,729],{"class":355},[287,1158,1159],{"class":336}," || '\u002F' \"\n",[287,1161,1162,1165,1167,1169,1171],{"class":207,"line":325},[287,1163,1164],{"class":336},"    \"|| lpad(@atlas_featurenumber, 2, '0') || '_' || ",[287,1166,729],{"class":355},[287,1168,732],{"class":336},[287,1170,729],{"class":355},[287,1172,737],{"class":336},[287,1174,1175],{"class":207,"line":343},[287,1176,340],{"class":296},[287,1178,1179,1181,1183],{"class":207,"line":362},[287,1180,346],{"class":292},[287,1182,568],{"class":292},[287,1184,1185],{"class":296}," atlas.filenameExpression():\n",[287,1187,1188,1190,1192,1195],{"class":207,"line":379},[287,1189,584],{"class":355},[287,1191,371],{"class":296},[287,1193,1194],{"class":336},"\"Filename expression rejected — falling back to page name\"",[287,1196,340],{"class":296},[287,1198,1199],{"class":207,"line":384},[287,1200,310],{"emptyLinePlaceholder":309},[287,1202,1203],{"class":207,"line":395},[287,1204,1205],{"class":525},"# Restrict the export to one region without touching the master filter\n",[287,1207,1208,1211,1213,1216,1218,1221,1223],{"class":207,"line":406},[287,1209,1210],{"class":296},"region_atlas_filter ",[287,1212,319],{"class":292},[287,1214,1215],{"class":336}," '\"region\" = ",[287,1217,551],{"class":355},[287,1219,1220],{"class":336},"North",[287,1222,551],{"class":355},[287,1224,1225],{"class":336},"'\n",[287,1227,1228],{"class":207,"line":411},[287,1229,1230],{"class":296},"atlas.setFilterExpression(region_atlas_filter)\n",[287,1232,1233],{"class":207,"line":433},[287,1234,1235],{"class":296},"atlas.updateFeatures()\n",[287,1237,1238,1240,1242,1244,1247,1249,1251,1253,1256],{"class":207,"line":439},[287,1239,447],{"class":355},[287,1241,371],{"class":296},[287,1243,452],{"class":292},[287,1245,1246],{"class":336},"\"Exporting ",[287,1248,458],{"class":355},[287,1250,652],{"class":296},[287,1252,464],{"class":355},[287,1254,1255],{"class":336}," North-region pages\"",[287,1257,340],{"class":296},[14,1259,1260,475,1262,1265,1266,1268,1269,1272,1273,1276,1277,1279,1280,511,1283,1285,1286,35],{},[48,1261,474],{},[27,1263,1264],{},"setFilenameExpression(...)"," overrides the plain ",[27,1267,701],{}," for file output — its result is the full path stem (without extension) for each per-feature file, so building it from ",[27,1270,1271],{},"\"region\""," sorts pages into subdirectories automatically. QGIS creates the intermediate folders as needed, but the parent (",[27,1274,1275],{},"\u002Fdata\u002Fatlas_out\u002F",") must exist. Because the expression is evaluated inside the iteration loop, every ",[27,1278,705],{}," variable and coverage field is in scope. Re-running ",[27,1281,1282],{},"setFilterExpression",[27,1284,683],{}," lets a single script emit several scoped map books — one per region — from the same layout, which is the backbone of a scheduled, unattended map-book job. For the broader pattern of running these steps unattended on a server, see ",[18,1287,1289],{"href":1288},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002F","batch processing with PyQGIS",[37,1291,1293],{"id":1292},"exporting-the-whole-series","Exporting the Whole Series",[14,1295,1296,1297,1300,1301,1304,1305,1307],{},"With the atlas configured, ",[27,1298,1299],{},"QgsLayoutExporter"," exports the series. ",[27,1302,1303],{},"exportToPdf"," (atlas overload) can produce one combined PDF or one PDF per feature; ",[27,1306,1129],{}," writes a folder of rasters. The same configured atlas fans out to three different output shapes depending on which overload you call.",[94,1309,100,1312,100,1315,100,1318,100,1322,100,1327,100,1331,100,1335,100,1341,100,1343,100,1346,100,1351,100,1356,100,1358,100,1362,100,1365,100,1369,100,1373,100,1375,100,1377,100,1382,100,1387,100,1391,100,1394,100,1398,100,1403,100,1406,100,1410,100,1414,100,1421,100,1426,100,1429],{"viewBox":1310,"role":97,"ariaLabel":1311,"xmlns":99},"0 0 772 292","Diagram comparing the three atlas export overloads and the output each produces",[102,1313,1314],{},"Three atlas export overloads and their outputs",[106,1316,1317],{},"One configured atlas with N pages feeds three exporter overloads: exportToPdf writes a single combined multi-page PDF; exportToPdfs writes one PDF per feature named from the page-name expression; exportToImage writes one raster per feature. A single-document glyph marks the combined output and a stacked-document glyph marks the per-feature outputs.",[110,1319],{"x":112,"y":112,"width":1320,"height":1321,"fill":115},"772","292",[110,1323],{"x":1324,"y":1325,"width":1326,"height":211,"rx":121,"fill":122,"stroke":123,"style":124},"16","116","132",[126,1328,1330],{"x":155,"y":1329,"fill":130,"style":131,"textAnchor":132},"147","Configured atlas",[126,1332,1334],{"x":155,"y":1333,"fill":137,"style":138,"textAnchor":132},"166","N pages",[207,1336],{"x1":1337,"y1":209,"x2":1338,"y2":1339,"stroke":137,"style":1340},"148","198","53","stroke-width:2.5;marker-end:url(#ax)",[207,1342],{"x1":1337,"y1":209,"x2":1338,"y2":209,"stroke":137,"style":1340},[207,1344],{"x1":1337,"y1":209,"x2":1338,"y2":1345,"stroke":137,"style":1340},"247",[110,1347],{"x":1348,"y":1349,"width":1350,"height":143,"rx":121,"fill":122,"stroke":146,"style":124},"200","33","176",[126,1352,1355],{"x":1353,"y":1354,"fill":130,"style":151,"textAnchor":132},"288","57","exportToPdf(atlas, …)",[110,1357],{"x":1348,"y":119,"width":1350,"height":143,"rx":121,"fill":122,"stroke":146,"style":124},[126,1359,1361],{"x":1353,"y":1360,"fill":130,"style":151,"textAnchor":132},"154","exportToPdfs(atlas, …)",[110,1363],{"x":1348,"y":1364,"width":1350,"height":143,"rx":121,"fill":122,"stroke":146,"style":124},"227",[126,1366,1368],{"x":1353,"y":1367,"fill":130,"style":151,"textAnchor":132},"251","exportToImage(atlas, …)",[207,1370],{"x1":1371,"y1":1339,"x2":1372,"y2":1339,"stroke":137,"style":1340},"376","426",[207,1374],{"x1":1371,"y1":209,"x2":1372,"y2":209,"stroke":137,"style":1340},[207,1376],{"x1":1371,"y1":1345,"x2":1372,"y2":1345,"stroke":137,"style":1340},[110,1378],{"x":225,"y":1379,"width":1380,"height":1381,"rx":121,"fill":182,"stroke":183,"style":124},"29","300","48",[126,1383,1386],{"x":1384,"y":1385,"fill":187,"style":151,"textAnchor":132},"578","49","One combined multi-page PDF",[126,1388,1390],{"x":1384,"y":150,"fill":187,"style":1389,"textAnchor":132},"text-anchor:middle;font-family:sans-serif;font-size:10.5px","district_atlas.pdf",[110,1392],{"x":225,"y":1393,"width":1380,"height":1381,"rx":121,"fill":122,"stroke":183,"style":124},"126",[126,1395,1397],{"x":1384,"y":1396,"fill":130,"style":151,"textAnchor":132},"146","One PDF per feature",[126,1399,1402],{"x":1384,"y":1400,"fill":137,"style":1401,"textAnchor":132},"162","text-anchor:middle;font-family:sans-serif;font-size:10px","N files · named by pageNameExpression",[110,1404],{"x":225,"y":1405,"width":1380,"height":1381,"rx":121,"fill":122,"stroke":183,"style":124},"223",[126,1407,1409],{"x":1384,"y":1408,"fill":130,"style":151,"textAnchor":132},"243","One image per feature",[126,1411,1413],{"x":1384,"y":1412,"fill":137,"style":1401,"textAnchor":132},"259","N rasters · e.g. PNG or JPG",[110,1415],{"x":1416,"y":1417,"width":1324,"height":1418,"rx":1419,"fill":182,"stroke":187,"style":1420},"738","41","24","2","stroke-width:1.5",[110,1422],{"x":1423,"y":1424,"width":1425,"height":1418,"rx":1419,"fill":122,"stroke":123,"style":1420},"736","138","17",[110,1427],{"x":1423,"y":1428,"width":1425,"height":1418,"rx":1419,"fill":122,"stroke":123,"style":1420},"235",[243,1430,245,1431,100],{},[247,1432,254,1434,245],{"id":1433,"markerWidth":250,"markerHeight":250,"refX":251,"refY":252,"orient":253},"ax",[233,1435],{"d":257,"fill":137},[278,1437,1439],{"className":280,"code":1438,"language":282,"meta":283,"style":283},"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,1440,1441,1448,1459,1463,1473,1488,1498,1502,1512,1522,1532,1536,1541,1551,1562,1567,1572,1578,1584,1590,1596,1601,1607,1613,1619,1625,1631,1636,1641,1655,1667,1675],{"__ignoreMap":283},[287,1442,1443,1445],{"class":207,"line":289},[287,1444,300],{"class":292},[287,1446,1447],{"class":296}," os\n",[287,1449,1450,1452,1454,1456],{"class":207,"line":306},[287,1451,293],{"class":292},[287,1453,297],{"class":296},[287,1455,300],{"class":292},[287,1457,1458],{"class":296}," QgsLayoutExporter\n",[287,1460,1461],{"class":207,"line":313},[287,1462,310],{"emptyLinePlaceholder":309},[287,1464,1465,1468,1470],{"class":207,"line":325},[287,1466,1467],{"class":296},"out_dir ",[287,1469,319],{"class":292},[287,1471,1472],{"class":336}," \"\u002Fdata\u002Fatlas_out\"\n",[287,1474,1475,1478,1482,1484,1486],{"class":207,"line":343},[287,1476,1477],{"class":296},"os.makedirs(out_dir, ",[287,1479,1481],{"class":1480},"s9osk","exist_ok",[287,1483,319],{"class":292},[287,1485,401],{"class":355},[287,1487,340],{"class":296},[287,1489,1490,1493,1495],{"class":207,"line":362},[287,1491,1492],{"class":296},"exporter ",[287,1494,319],{"class":292},[287,1496,1497],{"class":296}," QgsLayoutExporter(layout)\n",[287,1499,1500],{"class":207,"line":379},[287,1501,310],{"emptyLinePlaceholder":309},[287,1503,1504,1507,1509],{"class":207,"line":384},[287,1505,1506],{"class":296},"pdf_settings ",[287,1508,319],{"class":292},[287,1510,1511],{"class":296}," QgsLayoutExporter.PdfExportSettings()\n",[287,1513,1514,1517,1519],{"class":207,"line":395},[287,1515,1516],{"class":296},"pdf_settings.dpi ",[287,1518,319],{"class":292},[287,1520,1521],{"class":355}," 300\n",[287,1523,1524,1527,1529],{"class":207,"line":406},[287,1525,1526],{"class":296},"pdf_settings.forceVectorOutput ",[287,1528,319],{"class":292},[287,1530,1531],{"class":355}," True\n",[287,1533,1534],{"class":207,"line":411},[287,1535,310],{"emptyLinePlaceholder":309},[287,1537,1538],{"class":207,"line":433},[287,1539,1540],{"class":525},"# A) One combined multi-page PDF\n",[287,1542,1543,1546,1548],{"class":207,"line":439},[287,1544,1545],{"class":296},"result, error ",[287,1547,319],{"class":292},[287,1549,1550],{"class":296}," exporter.exportToPdf(\n",[287,1552,1553,1556,1559],{"class":207,"line":444},[287,1554,1555],{"class":296},"    atlas, os.path.join(out_dir, ",[287,1557,1558],{"class":336},"\"district_atlas.pdf\"",[287,1560,1561],{"class":296},"), pdf_settings\n",[287,1563,1565],{"class":207,"line":1564},15,[287,1566,340],{"class":296},[287,1568,1570],{"class":207,"line":1569},16,[287,1571,310],{"emptyLinePlaceholder":309},[287,1573,1575],{"class":207,"line":1574},17,[287,1576,1577],{"class":525},"# B) One PDF per feature (filenames from pageNameExpression)\n",[287,1579,1581],{"class":207,"line":1580},18,[287,1582,1583],{"class":525},"# result, error = exporter.exportToPdfs(\n",[287,1585,1587],{"class":207,"line":1586},19,[287,1588,1589],{"class":525},"#     atlas, os.path.join(out_dir, \"districts\"), pdf_settings\n",[287,1591,1593],{"class":207,"line":1592},20,[287,1594,1595],{"class":525},"# )\n",[287,1597,1599],{"class":207,"line":1598},21,[287,1600,310],{"emptyLinePlaceholder":309},[287,1602,1604],{"class":207,"line":1603},22,[287,1605,1606],{"class":525},"# C) One PNG per feature\n",[287,1608,1610],{"class":207,"line":1609},23,[287,1611,1612],{"class":525},"# img_settings = QgsLayoutExporter.ImageExportSettings()\n",[287,1614,1616],{"class":207,"line":1615},24,[287,1617,1618],{"class":525},"# img_settings.dpi = 200\n",[287,1620,1622],{"class":207,"line":1621},25,[287,1623,1624],{"class":525},"# result, error = exporter.exportToImage(\n",[287,1626,1628],{"class":207,"line":1627},26,[287,1629,1630],{"class":525},"#     atlas, os.path.join(out_dir, \"districts\"), \"png\", img_settings\n",[287,1632,1634],{"class":207,"line":1633},27,[287,1635,1595],{"class":525},[287,1637,1639],{"class":207,"line":1638},28,[287,1640,310],{"emptyLinePlaceholder":309},[287,1642,1644,1646,1649,1652],{"class":207,"line":1643},29,[287,1645,346],{"class":292},[287,1647,1648],{"class":296}," result ",[287,1650,1651],{"class":292},"==",[287,1653,1654],{"class":296}," QgsLayoutExporter.Success:\n",[287,1656,1658,1660,1662,1665],{"class":207,"line":1657},30,[287,1659,584],{"class":355},[287,1661,371],{"class":296},[287,1663,1664],{"class":336},"\"Atlas exported successfully\"",[287,1666,340],{"class":296},[287,1668,1670,1673],{"class":207,"line":1669},31,[287,1671,1672],{"class":292},"else",[287,1674,359],{"class":296},[287,1676,1678,1680,1682,1685],{"class":207,"line":1677},32,[287,1679,584],{"class":355},[287,1681,371],{"class":296},[287,1683,1684],{"class":336},"\"Export failed:\"",[287,1686,1687],{"class":296},", error)\n",[14,1689,1690,1692,1693,1696,1697,1700,1701,1704,1705,1707,1708,1711,1712,1715,1716,1718,1719,1722,1723,1725,1726,1730],{},[48,1691,474],{}," The atlas-aware overloads take the ",[27,1694,1695],{},"atlas"," object as their first argument, which is what makes them iterate instead of rendering a single page. ",[27,1698,1699],{},"exportToPdf(atlas, ...)"," concatenates all pages into one document; ",[27,1702,1703],{},"exportToPdfs(atlas, base, ...)"," writes a separate file per feature named from the ",[27,1706,701],{},". ",[27,1709,1710],{},"exportToImage(atlas, base, \"png\", ...)"," does the same for rasters. The atlas overloads return a ",[27,1713,1714],{},"(result, error)"," tuple — unlike the single-layout ",[27,1717,1303],{},", which returns only a code — so unpack two values and check ",[27,1720,1721],{},"result == QgsLayoutExporter.Success",". For a complete standalone script around option A, see ",[18,1724,34],{"href":33},". The same exporter underpins ",[18,1727,1729],{"href":1728},"\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,1732,1734],{"id":1733},"building-a-layout-and-atlas-entirely-from-code","Building a Layout and Atlas Entirely from Code",[14,1736,1737,1738,1740],{},"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,1739,784],{},", flagging it atlas-driven, then wiring the atlas to it.",[278,1742,1744],{"className":280,"code":1743,"language":282,"meta":283,"style":283},"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,1745,1746,1757,1762,1767,1771,1775,1783,1799,1803,1812,1817,1827,1832,1836,1841,1850,1865,1879,1890,1899,1903,1912,1917,1921,1926,1934,1942,1946,1950],{"__ignoreMap":283},[287,1747,1748,1750,1752,1754],{"class":207,"line":289},[287,1749,293],{"class":292},[287,1751,297],{"class":296},[287,1753,300],{"class":292},[287,1755,1756],{"class":296}," (\n",[287,1758,1759],{"class":207,"line":306},[287,1760,1761],{"class":296},"    QgsProject, QgsPrintLayout, QgsLayoutItemMap,\n",[287,1763,1764],{"class":207,"line":313},[287,1765,1766],{"class":296},"    QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes,\n",[287,1768,1769],{"class":207,"line":325},[287,1770,340],{"class":296},[287,1772,1773],{"class":207,"line":343},[287,1774,310],{"emptyLinePlaceholder":309},[287,1776,1777,1779,1781],{"class":207,"line":362},[287,1778,316],{"class":296},[287,1780,319],{"class":292},[287,1782,322],{"class":296},[287,1784,1785,1787,1789,1791,1793,1795,1797],{"class":207,"line":379},[287,1786,414],{"class":296},[287,1788,319],{"class":292},[287,1790,419],{"class":296},[287,1792,422],{"class":336},[287,1794,425],{"class":296},[287,1796,112],{"class":355},[287,1798,430],{"class":296},[287,1800,1801],{"class":207,"line":384},[287,1802,310],{"emptyLinePlaceholder":309},[287,1804,1805,1807,1809],{"class":207,"line":395},[287,1806,328],{"class":296},[287,1808,319],{"class":292},[287,1810,1811],{"class":296}," QgsPrintLayout(project)\n",[287,1813,1814],{"class":207,"line":406},[287,1815,1816],{"class":296},"layout.initializeDefaults()\n",[287,1818,1819,1822,1825],{"class":207,"line":411},[287,1820,1821],{"class":296},"layout.setName(",[287,1823,1824],{"class":336},"\"GeneratedAtlas\"",[287,1826,340],{"class":296},[287,1828,1829],{"class":207,"line":433},[287,1830,1831],{"class":296},"project.layoutManager().addLayout(layout)\n",[287,1833,1834],{"class":207,"line":439},[287,1835,310],{"emptyLinePlaceholder":309},[287,1837,1838],{"class":207,"line":444},[287,1839,1840],{"class":525},"# Atlas-controlled map item\n",[287,1842,1843,1845,1847],{"class":207,"line":1564},[287,1844,814],{"class":296},[287,1846,319],{"class":292},[287,1848,1849],{"class":296}," QgsLayoutItemMap(layout)\n",[287,1851,1852,1855,1858,1860,1862],{"class":207,"line":1569},[287,1853,1854],{"class":296},"map_item.attemptMove(QgsLayoutPoint(",[287,1856,1857],{"class":355},"10",[287,1859,976],{"class":296},[287,1861,1857],{"class":355},[287,1863,1864],{"class":296},", QgsUnitTypes.LayoutMillimeters))\n",[287,1866,1867,1870,1873,1875,1877],{"class":207,"line":1574},[287,1868,1869],{"class":296},"map_item.attemptResize(QgsLayoutSize(",[287,1871,1872],{"class":355},"190",[287,1874,976],{"class":296},[287,1876,224],{"class":355},[287,1878,1864],{"class":296},[287,1880,1881,1884,1887],{"class":207,"line":1580},[287,1882,1883],{"class":296},"map_item.setLayers(",[287,1885,1886],{"class":355},"list",[287,1888,1889],{"class":296},"(project.mapLayers().values()))\n",[287,1891,1892,1895,1897],{"class":207,"line":1586},[287,1893,1894],{"class":296},"map_item.setAtlasDriven(",[287,1896,401],{"class":355},[287,1898,340],{"class":296},[287,1900,1901],{"class":207,"line":1592},[287,1902,874],{"class":296},[287,1904,1905,1907,1910],{"class":207,"line":1598},[287,1906,879],{"class":296},[287,1908,1909],{"class":355},"0.1",[287,1911,340],{"class":296},[287,1913,1914],{"class":207,"line":1603},[287,1915,1916],{"class":296},"layout.addLayoutItem(map_item)\n",[287,1918,1919],{"class":207,"line":1609},[287,1920,310],{"emptyLinePlaceholder":309},[287,1922,1923],{"class":207,"line":1615},[287,1924,1925],{"class":525},"# Wire the atlas\n",[287,1927,1928,1930,1932],{"class":207,"line":1621},[287,1929,387],{"class":296},[287,1931,319],{"class":292},[287,1933,392],{"class":296},[287,1935,1936,1938,1940],{"class":207,"line":1627},[287,1937,398],{"class":296},[287,1939,401],{"class":355},[287,1941,340],{"class":296},[287,1943,1944],{"class":207,"line":1633},[287,1945,436],{"class":296},[287,1947,1948],{"class":207,"line":1638},[287,1949,1235],{"class":296},[287,1951,1952,1954,1956,1958,1961,1963,1965,1967,1969],{"class":207,"line":1643},[287,1953,447],{"class":355},[287,1955,371],{"class":296},[287,1957,452],{"class":292},[287,1959,1960],{"class":336},"\"Generated atlas with ",[287,1962,458],{"class":355},[287,1964,652],{"class":296},[287,1966,464],{"class":355},[287,1968,657],{"class":336},[287,1970,340],{"class":296},[14,1972,1973,475,1975,511,1978,1981,1982,1985,1986,1988,1989,1992,1993,1996,1997,2000,2001,2003],{},[48,1974,474],{},[27,1976,1977],{},"QgsPrintLayout(project)",[27,1979,1980],{},"initializeDefaults()"," creates a blank A4 layout; ",[27,1983,1984],{},"addLayout"," registers it so the layout manager (and later export) can find it. The ",[27,1987,784],{}," is positioned and sized in millimetres, then ",[27,1990,1991],{},"setAtlasDriven(True)"," is the switch that makes this map follow the atlas — without it the atlas iterates but the map never reframes. ",[27,1994,1995],{},"setAtlasScalingMode"," and ",[27,1998,1999],{},"setAtlasMargin"," govern per-page framing exactly as on a GUI-built layout. This mirrors the single-layout construction in ",[18,2002,84],{"href":83},", adding the atlas wiring on top.",[37,2005,2007],{"id":2006},"compatibility-notes","Compatibility Notes",[2009,2010,2011,2024],"table",{},[2012,2013,2014],"thead",{},[2015,2016,2017,2021],"tr",{},[2018,2019,2020],"th",{},"QGIS \u002F Python",[2018,2022,2023],{},"Atlas API notes",[2025,2026,2027,2045,2060],"tbody",{},[2015,2028,2029,2033],{},[2030,2031,2032],"td",{},"3.28 LTR \u002F Py 3.9",[2030,2034,2035,2037,2038,2041,2042,2044],{},[27,2036,271],{}," API stable. Enum access via flat names (e.g. ",[27,2039,2040],{},"QgsLayoutItemMap.Auto",") works. ",[27,2043,1282],{}," may not return a bool.",[2015,2046,2047,2052],{},[2030,2048,2049],{},[48,2050,2051],{},"3.34 LTR \u002F Py 3.12 (baseline)",[2030,2053,2054,2055,2057,2058,35],{},"Recommended. Scoped enums (",[27,2056,905],{},") available; atlas export overloads return ",[27,2059,1714],{},[2015,2061,2062,2065],{},[2030,2063,2064],{},"3.40 \u002F 3.44 \u002F Py 3.12",[2030,2066,2067],{},"Same model. Minor expression-function additions; re-verify any expression-driven settings.",[14,2069,2070,2071,2074,2075,2077],{},"Across versions, always call ",[27,2072,2073],{},"atlas.updateFeatures()"," after changing the coverage layer, filter, or sort, and always verify ",[27,2076,652],{}," before a long export so you do not render an empty or unexpectedly large series.",[37,2079,2081],{"id":2080},"what-changes-between-pages-and-what-does-not","What changes between pages, and what does not",[14,2083,2084],{},"An atlas re-renders the whole layout for every coverage feature, but only some things vary. Knowing which is which explains both what you can drive per page and why some elements stubbornly stay the same.",[14,2086,2087],{},[94,2088,2091,2094,2097,2101,2107,2112,2118,2141,2144,2148],{"viewBox":2089,"role":97,"ariaLabel":2090,"xmlns":99},"0 0 760 246","Atlas page elements split into those that vary per feature — extent, expression text, filters and output path — and those that stay fixed, such as page size, item positions and styling",[102,2092,2093],{},"What varies per atlas page",[106,2095,2096],{},"Varying per page: the map extent driven by the coverage feature, any label text built from an expression, data-defined filters and visibility, and the output filename. Fixed for the whole series: the page size and orientation, the position and size of every layout item, and the layer styling.",[110,2098],{"x":112,"y":112,"width":2099,"height":2100,"fill":115},"760","246",[126,2102,2106],{"x":2103,"y":2104,"style":2105,"fill":130,"textAnchor":132},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","The layout is the template; the coverage feature is the variable",[110,2108],{"x":1324,"y":2109,"width":2110,"height":2111,"rx":1857,"fill":122,"stroke":183,"style":124},"46","356","184",[126,2113,2117],{"x":2114,"y":2115,"style":2116,"fill":183,"textAnchor":132},"194","70","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","varies per page",[2119,2120,2122,2127,2130,2133,2137],"g",{"style":2121},"font-size:11.5px;font-family:sans-serif",[126,2123,2126],{"x":2124,"y":2125,"fill":137},"44","102","the map extent",[126,2128,2129],{"x":2124,"y":119,"fill":137},"expression-driven label text",[126,2131,2132],{"x":2124,"y":162,"fill":137},"data-defined filters and visibility",[126,2134,2136],{"x":2124,"y":2135,"fill":137},"186","the output filename",[126,2138,2140],{"x":2124,"y":2139,"fill":183},"212","anything reading @atlas_feature",[110,2142],{"x":2143,"y":2109,"width":2110,"height":2111,"rx":1857,"fill":122,"stroke":196,"style":124},"388",[126,2145,2147],{"x":2146,"y":2115,"style":2116,"fill":196,"textAnchor":132},"566","fixed for the series",[2119,2149,2150,2154,2157,2160,2163],{"style":2121},[126,2151,2153],{"x":2152,"y":2125,"fill":137},"416","page size and orientation",[126,2155,2156],{"x":2152,"y":119,"fill":137},"where every item sits",[126,2158,2159],{"x":2152,"y":162,"fill":137},"layer styling and symbology",[126,2161,2162],{"x":2152,"y":2135,"fill":137},"which layers are in the map",[126,2164,2165],{"x":2152,"y":2139,"fill":196},"change these and you change every page",[14,2167,2168,2169,2172],{},"The dividing line is ",[27,2170,2171],{},"@atlas_feature"," and the variables derived from it. Anything whose value comes from an expression that references the current coverage feature varies; everything else is part of the template. That is why a title that should read \"Ashford District\" on one page and \"Brant District\" on the next has to be an expression rather than typed text, and why changing the symbology mid-series is not something the atlas can do for you.",[37,2174,2176],{"id":2175},"key-takeaways","Key Takeaways",[42,2178,2179,2191,2200,2208,2218,2231,2238],{},[45,2180,2181,2182,2184,2185,2187,2188,2190],{},"An atlas is a ",[27,2183,271],{}," owned by a ",[27,2186,267],{},"; reach it via ",[27,2189,275],{},", enable it, and bind a coverage layer.",[45,2192,2193,2194,2196,2197,2199],{},"Filtering and sorting shape the series — call ",[27,2195,683],{}," afterward and check ",[27,2198,652],{}," to confirm the page list.",[45,2201,2202,2204,2205,2207],{},[27,2203,701],{}," (with ",[27,2206,752],{}," and field references) names per-feature outputs and feeds dynamic labels.",[45,2209,2210,2211,2213,2214,1996,2216,35],{},"Per-page framing is controlled on the atlas-driven ",[27,2212,784],{}," via ",[27,2215,1995],{},[27,2217,1999],{},[45,2219,2220,2221,673,2223,673,2225,673,2227,2230],{},"Step through pages with ",[27,2222,1072],{},[27,2224,1076],{},[27,2226,1079],{},[27,2228,2229],{},"seekTo()"," to preview extents and labels before a long export.",[45,2232,2233,2234,2237],{},"A data-defined ",[27,2235,2236],{},"filenameExpression"," sorts per-feature output into subfolders and lets one script emit several scoped map books.",[45,2239,2240,2241,2243,2244,976,2246,976,2248,2250,2251,2253],{},"The atlas overloads of ",[27,2242,1299],{}," (",[27,2245,1303],{},[27,2247,1126],{},[27,2249,1129],{},") iterate the series and return a ",[27,2252,1714],{}," tuple.",[37,2255,2257],{"id":2256},"frequently-asked-questions","Frequently Asked Questions",[14,2259,2260,2263,2264,2266,2267,2269,2270,2272,2273,2275,2276,35],{},[48,2261,2262],{},"How do I get the atlas object from a layout?","\nCall ",[27,2265,275],{}," on a ",[27,2268,267],{},". Each print layout owns exactly one ",[27,2271,271],{},". Enable it with ",[27,2274,492],{}," and bind the coverage layer with ",[27,2277,2278],{},"setCoverageLayer(...)",[14,2280,2281,2284,2285,2287,2288,2291,2292,2294],{},[48,2282,2283],{},"Why does my atlas export zero pages?","\nUsually an invalid or over-restrictive filter, or you forgot ",[27,2286,683],{}," after changing the filter. Check ",[27,2289,2290],{},"atlas.filterExpression()"," is valid and inspect ",[27,2293,652],{}," before exporting. A geographic mismatch between coverage and map CRS can also exclude everything.",[14,2296,2297,2305,2308,2309,2312,2313,2315],{},[48,2298,2299,2300,1996,2302,2304],{},"What is the difference between ",[27,2301,1303],{},[27,2303,1126],{},"?",[27,2306,2307],{},"exportToPdf(atlas, path, settings)"," writes one combined multi-page PDF. ",[27,2310,2311],{},"exportToPdfs(atlas, base, settings)"," writes one PDF per feature, named from ",[27,2314,701],{},". Both take the atlas object so they iterate the series.",[14,2317,2318,2321,2322,2324,2325,2327],{},[48,2319,2320],{},"How do I make titles and labels change per page?","\nUse expression-based layout labels referencing coverage fields, e.g. ",[27,2323,764],{},", plus atlas variables like ",[27,2326,752],{},". The atlas re-evaluates them for each feature during iteration.",[14,2329,2330,2333,2334,2337,2338,2341,2342,2345],{},[48,2331,2332],{},"Can I run an atlas export headlessly on a server?","\nYes. Bootstrap ",[27,2335,2336],{},"QgsApplication",", read the project with ",[27,2339,2340],{},"QgsProject.instance().read(...)",", configure the atlas, and call the atlas exporter overloads. The dedicated ",[18,2343,2344],{"href":33},"Generate an Atlas PDF in PyQGIS"," guide shows the full standalone script.",[14,2347,2348,2263,2351,2354,2355,2358,2359,2362,2363,1996,2366,2369,2370,2373],{},[48,2349,2350],{},"How do I preview a single atlas page without exporting?",[27,2352,2353],{},"atlas.beginRender()",", then ",[27,2356,2357],{},"atlas.first()"," or ",[27,2360,2361],{},"atlas.seekTo(index)"," to move the current page; inspect ",[27,2364,2365],{},"atlas.currentFeature()",[27,2367,2368],{},"atlas.nameForPage(...)",", and finish with ",[27,2371,2372],{},"atlas.endRender()",". This reframes the controlled map and re-evaluates labels exactly like an export, but writes nothing to disk.",[14,2375,2376,2379,2380,2383,2384,2386,2387,58,2389,2391],{},[48,2377,2378],{},"Can I send each page to a different folder?","\nYes. Set a data-defined filename with ",[27,2381,2382],{},"atlas.setFilenameExpression(...)"," that builds a path from coverage fields — for example prefixing ",[27,2385,1271],{}," as a subfolder. QGIS evaluates it per feature during ",[27,2388,1126],{},[27,2390,1129],{}," and creates missing subdirectories under an existing parent.",[14,2393,2394,2397],{},[48,2395,2396],{},"Can each atlas page show different layers?","\nYes, through data-defined visibility driven by an expression referencing the coverage feature — but the layers themselves must all be in the map item to begin with.",[14,2399,2400,2403],{},[48,2401,2402],{},"Can an atlas have more pages than coverage features?","\nNo — one page per feature after filtering, which is why the filter expression is the way to control the page count.",[14,2405,2406,2409],{},[48,2407,2408],{},"Can the coverage layer be different from the layers shown on the map?","\nYes, and it usually is. The coverage layer supplies the features the atlas iterates and the extents it frames; it does not have to be drawn at all, and hiding it is common when it is a grid of tiles rather than something the reader should see.",[37,2411,2413],{"id":2412},"related","Related",[42,2415,2416,2420,2424,2430,2435,2439,2446],{},[45,2417,2418],{},[18,2419,21],{"href":20},[45,2421,2422],{},[18,2423,84],{"href":83},[45,2425,2426],{},[18,2427,2429],{"href":2428},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","Chaining Processing Algorithms in PyQGIS",[45,2431,2432],{},[18,2433,2434],{"href":1288},"Batch Processing with PyQGIS",[45,2436,2437],{},[18,2438,2344],{"href":33},[45,2440,2441,2445],{},[18,2442,2444],{"href":2443},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fconfigure-atlas-coverage-layer-pyqgis\u002F","Configure an Atlas Coverage Layer in PyQGIS"," — choose the driving layer, filter and sort the pages, and frame each feature.",[45,2447,2448,2452],{},[18,2449,2451],{"href":2450},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fexport-atlas-pages-to-individual-pdfs-pyqgis\u002F","Export Atlas Pages to Individual PDFs"," — one named file per feature instead of a single document.",[2454,2455,2456],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":283,"searchDepth":306,"depth":306,"links":2458},[2459,2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2471,2472,2473],{"id":39,"depth":306,"text":40},{"id":88,"depth":306,"text":89},{"id":260,"depth":306,"text":261},{"id":503,"depth":306,"text":504},{"id":694,"depth":306,"text":695},{"id":777,"depth":306,"text":778},{"id":921,"depth":306,"text":922},{"id":1115,"depth":306,"text":1116},{"id":1292,"depth":306,"text":1293},{"id":1733,"depth":306,"text":1734},{"id":2006,"depth":306,"text":2007},{"id":2080,"depth":306,"text":2081},{"id":2175,"depth":306,"text":2176},{"id":2256,"depth":306,"text":2257},{"id":2412,"depth":306,"text":2413},"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",{"slug":2477,"type":2478,"breadcrumb":2479,"datePublished":2480,"dateModified":2481},"automating-atlas-map-series","guide","Automating Atlas Map Series","2025-03-11","2026-07-18","\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series",{"title":5,"description":2474},"spatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Findex","7A6bwNcSBLZj_8lg5oMXbZ3yuWuQOzoJKBL1fEGnTyo",1787823363418]