[{"data":1,"prerenderedAt":1389},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fatlas-expressions-and-dynamic-text-pyqgis":3},{"id":4,"title":5,"body":6,"description":1378,"extension":1379,"meta":1380,"navigation":253,"path":1385,"seo":1386,"stem":1387,"__hash__":1388},"docs\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fatlas-expressions-and-dynamic-text-pyqgis\u002Findex.md","Atlas Expressions and Dynamic Text in PyQGIS",{"type":7,"value":8,"toc":1363},"minimark",[9,13,17,26,192,197,216,220,416,434,438,490,502,506,597,683,712,716,719,762,779,782,811,826,830,835,882,891,895,973,999,1002,1118,1126,1130,1136,1172,1181,1184,1188,1214,1218,1267,1271,1283,1287,1293,1305,1315,1325,1329,1359],[10,11,5],"h1",{"id":12},"atlas-expressions-and-dynamic-text-in-pyqgis",[14,15,16],"p",{},"An atlas without expressions produces four hundred identical maps at four hundred different places. The expressions are what make each page name itself, title itself, filter its own labels and land in a sensibly named file — and all of them are properties you can set from Python, which is what turns an atlas into something a scheduled job produces rather than something a person clicks through.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002F","Automating Atlas Map Series",". It covers the atlas variables, wiring filename and page name expressions, dynamic labels, filtering and sorting the coverage, and testing a handful of pages before committing to the whole run.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,112,117,122,128,134,138,144,150,154,157,161,164,168,171,175,178,183,188],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 320","img","The atlas variables available while a page renders: the current feature, its number, the total, the page name and the map scale","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"What an atlas puts into the expression context",[40,41,42],"desc",{},"While each page renders, the atlas adds variables to the expression context: the current coverage feature, its position in the sequence, the total number of features, the page name and the geometry of the current feature. Every label, filename and filter on the page can read them.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","320","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"atxArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","One row of the coverage layer per page",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"24","48","212","176","10","#eef7f4","#0f766e","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":84,"textAnchor":74},"130","74","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","coverage layer",[93,94,97,103,106,109],"g",{"fill":95,"stroke":84,"style":96},"#e8efe6","stroke-width:1.2",[44,98],{"x":99,"y":100,"width":101,"height":102},"44","94","172","18",[44,104],{"x":99,"y":105,"width":101,"height":102},"116",[44,107],{"x":99,"y":108,"width":101,"height":102},"138",[44,110],{"x":99,"y":111,"width":101,"height":102},"160",[44,113],{"x":99,"y":105,"width":101,"height":102,"fill":114,"stroke":115,"style":116},"#fdf2e2","#b45309","stroke-width:2",[68,118,121],{"x":88,"y":119,"style":120,"fill":115,"textAnchor":74},"202","text-anchor:middle;font-size:10px;font-family:sans-serif","page 2 is rendering",[123,124],"line",{"x1":125,"y1":88,"x2":126,"y2":88,"stroke":66,"style":127},"236","270","stroke-width:2;marker-end:url(#atxArrow)",[44,129],{"x":130,"y":79,"width":131,"height":81,"rx":82,"fill":132,"stroke":133,"style":85},"280","456","#fffdf7","#2563eb",[68,135,137],{"x":136,"y":89,"style":90,"fill":133,"textAnchor":74},"508","variables available on that page",[68,139,143],{"x":140,"y":141,"style":142,"fill":66},"302","102","font-size:10.5px;font-family:monospace","@atlas_feature",[68,145,149],{"x":146,"y":141,"style":147,"fill":148},"520","font-size:10px;font-family:sans-serif","#59645f","the whole feature",[68,151,153],{"x":140,"y":152,"style":142,"fill":66},"128","@atlas_featurenumber",[68,155,156],{"x":146,"y":152,"style":147,"fill":148},"2",[68,158,160],{"x":140,"y":159,"style":142,"fill":66},"154","@atlas_totalfeatures",[68,162,163],{"x":146,"y":159,"style":147,"fill":148},"418",[68,165,167],{"x":140,"y":166,"style":142,"fill":66},"180","@atlas_pagename",[68,169,170],{"x":146,"y":166,"style":147,"fill":148},"from your expression",[68,172,174],{"x":140,"y":173,"style":142,"fill":66},"206","@atlas_geometry",[68,176,177],{"x":146,"y":173,"style":147,"fill":148},"for spatial filters",[44,179],{"x":180,"y":181,"width":146,"height":182,"rx":58,"fill":114,"stroke":115,"style":116},"120","252","52",[68,184,187],{"x":70,"y":185,"style":186,"fill":66,"textAnchor":74},"276","text-anchor:middle;font-size:11px;font-family:sans-serif","read a field with attribute(@atlas_feature, 'name'), not a bare field name",[68,189,191],{"x":70,"y":190,"style":120,"fill":148,"textAnchor":74},"296","a bare name resolves against whatever layer the item belongs to",[193,194,196],"h2",{"id":195},"prerequisites","Prerequisites",[198,199,200,208],"ul",{},[201,202,203,207],"li",{},[204,205,206],"strong",{},"QGIS 3.34 LTR"," or newer.",[201,209,210,211,215],{},"A layout with a map item, and a coverage layer — see ",[21,212,214],{"href":213},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fconfigure-atlas-coverage-layer-pyqgis\u002F","configuring an atlas coverage layer",".",[193,217,219],{"id":218},"wire-the-atlas-from-python","Wire the atlas from Python",[221,222,227],"pre",{"className":223,"code":224,"language":225,"meta":226,"style":226},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nproject = QgsProject.instance()\nlayout = project.layoutManager().layoutByName(\"site_atlas\")\nsites = project.mapLayersByName(\"sites\")[0]\n\natlas = layout.atlas()\natlas.setEnabled(True)\natlas.setCoverageLayer(sites)\natlas.setPageNameExpression('\"district\" || \\' — \\' || \"site_id\"')\natlas.setFilenameExpression(\"'site_' || \\\"site_id\\\" || '_' || format_date(now(), 'yyyyMMdd')\")\natlas.setSortFeatures(True)\natlas.setSortExpression('\"district\"')\natlas.setSortAscending(True)\n","python","",[228,229,230,248,255,267,285,308,313,324,335,341,363,385,395,406],"code",{"__ignoreMap":226},[231,232,234,238,242,245],"span",{"class":123,"line":233},1,[231,235,237],{"class":236},"snl16","from",[231,239,241],{"class":240},"s95oV"," qgis.core ",[231,243,244],{"class":236},"import",[231,246,247],{"class":240}," QgsProject\n",[231,249,251],{"class":123,"line":250},2,[231,252,254],{"emptyLinePlaceholder":253},true,"\n",[231,256,258,261,264],{"class":123,"line":257},3,[231,259,260],{"class":240},"project ",[231,262,263],{"class":236},"=",[231,265,266],{"class":240}," QgsProject.instance()\n",[231,268,270,273,275,278,282],{"class":123,"line":269},4,[231,271,272],{"class":240},"layout ",[231,274,263],{"class":236},[231,276,277],{"class":240}," project.layoutManager().layoutByName(",[231,279,281],{"class":280},"sU2Wk","\"site_atlas\"",[231,283,284],{"class":240},")\n",[231,286,288,291,293,296,299,302,305],{"class":123,"line":287},5,[231,289,290],{"class":240},"sites ",[231,292,263],{"class":236},[231,294,295],{"class":240}," project.mapLayersByName(",[231,297,298],{"class":280},"\"sites\"",[231,300,301],{"class":240},")[",[231,303,46],{"class":304},"sDLfK",[231,306,307],{"class":240},"]\n",[231,309,311],{"class":123,"line":310},6,[231,312,254],{"emptyLinePlaceholder":253},[231,314,316,319,321],{"class":123,"line":315},7,[231,317,318],{"class":240},"atlas ",[231,320,263],{"class":236},[231,322,323],{"class":240}," layout.atlas()\n",[231,325,327,330,333],{"class":123,"line":326},8,[231,328,329],{"class":240},"atlas.setEnabled(",[231,331,332],{"class":304},"True",[231,334,284],{"class":240},[231,336,338],{"class":123,"line":337},9,[231,339,340],{"class":240},"atlas.setCoverageLayer(sites)\n",[231,342,344,347,350,353,356,358,361],{"class":123,"line":343},10,[231,345,346],{"class":240},"atlas.setPageNameExpression(",[231,348,349],{"class":280},"'\"district\" || ",[231,351,352],{"class":304},"\\'",[231,354,355],{"class":280}," — ",[231,357,352],{"class":304},[231,359,360],{"class":280}," || \"site_id\"'",[231,362,284],{"class":240},[231,364,366,369,372,375,378,380,383],{"class":123,"line":365},11,[231,367,368],{"class":240},"atlas.setFilenameExpression(",[231,370,371],{"class":280},"\"'site_' || ",[231,373,374],{"class":304},"\\\"",[231,376,377],{"class":280},"site_id",[231,379,374],{"class":304},[231,381,382],{"class":280}," || '_' || format_date(now(), 'yyyyMMdd')\"",[231,384,284],{"class":240},[231,386,388,391,393],{"class":123,"line":387},12,[231,389,390],{"class":240},"atlas.setSortFeatures(",[231,392,332],{"class":304},[231,394,284],{"class":240},[231,396,398,401,404],{"class":123,"line":397},13,[231,399,400],{"class":240},"atlas.setSortExpression(",[231,402,403],{"class":280},"'\"district\"'",[231,405,284],{"class":240},[231,407,409,412,414],{"class":123,"line":408},14,[231,410,411],{"class":240},"atlas.setSortAscending(",[231,413,332],{"class":304},[231,415,284],{"class":240},[14,417,418,421,422,425,426,429,430,433],{},[204,419,420],{},"Breakdown:"," Inside these expressions, bare field names refer to the ",[423,424,91],"em",{},", because the atlas evaluates them against the current coverage feature — which is why ",[228,427,428],{},"\"site_id\""," works here without ",[228,431,432],{},"attribute(@atlas_feature, ...)",". The filename expression must not include a directory or an extension; the exporter supplies both, and putting a slash in it produces a confusing failure. Sorting matters more than it looks on a printed atlas: an unsorted coverage produces pages in provider order, which is arbitrary and changes between exports, making a page number useless as a reference.",[193,435,437],{"id":436},"filtering-the-coverage","Filtering the coverage",[221,439,441],{"className":223,"code":440,"language":225,"meta":226,"style":226},"atlas.setFilterFeatures(True)\natlas.setFilterExpression('\"status\" = \\'open\\' AND \"area_m2\" > 500')\n\nprint(\"pages:\", atlas.count())\n",[228,442,443,452,472,476],{"__ignoreMap":226},[231,444,445,448,450],{"class":123,"line":233},[231,446,447],{"class":240},"atlas.setFilterFeatures(",[231,449,332],{"class":304},[231,451,284],{"class":240},[231,453,454,457,460,462,465,467,470],{"class":123,"line":250},[231,455,456],{"class":240},"atlas.setFilterExpression(",[231,458,459],{"class":280},"'\"status\" = ",[231,461,352],{"class":304},[231,463,464],{"class":280},"open",[231,466,352],{"class":304},[231,468,469],{"class":280}," AND \"area_m2\" > 500'",[231,471,284],{"class":240},[231,473,474],{"class":123,"line":257},[231,475,254],{"emptyLinePlaceholder":253},[231,477,478,481,484,487],{"class":123,"line":269},[231,479,480],{"class":304},"print",[231,482,483],{"class":240},"(",[231,485,486],{"class":280},"\"pages:\"",[231,488,489],{"class":240},", atlas.count())\n",[14,491,492,494,495,498,499,501],{},[204,493,420],{}," As with layout tables, the boolean switch and the expression are separate, and setting the expression alone does nothing. ",[228,496,497],{},"count()"," re-evaluates the filter and returns how many pages the export will produce, which is the single most useful thing to print before starting a long run — a filter that accidentally matches everything turns a twelve-page atlas into a four-hundred-page one, and finding that out from ",[228,500,497],{}," costs a second.",[193,503,505],{"id":504},"dynamic-text-on-the-page","Dynamic text on the page",[14,507,508],{},[29,509,512,515,518,521,524,529,537,542,548,553,557,562,566,569,573,577,580,585,589,592],{"viewBox":510,"role":32,"ariaLabel":511,"xmlns":34},"0 0 760 306","A layout page showing which items read atlas variables: a title label, a page counter, a map filtered to the current feature, and a table of related records",[36,513,514],{},"Four items, all reading the same feature",[40,516,517],{},"On each atlas page the title label reads the current feature's name, the footer reads its position and the total, the map centres on its geometry, and a table lists records related to it. All four are driven by the same coverage feature without any per-page editing.",[44,519],{"x":46,"y":46,"width":47,"height":520,"fill":49},"306",[68,522,523],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"One feature drives the whole page",[44,525],{"x":166,"y":526,"width":527,"height":80,"rx":528,"fill":132,"stroke":148,"style":85},"46","400","6",[44,530],{"x":531,"y":532,"width":533,"height":534,"rx":535,"fill":83,"stroke":84,"style":536},"200","62","360","28","3","stroke-width:1.8",[68,538,541],{"x":70,"y":539,"style":540,"fill":84,"textAnchor":74},"81","text-anchor:middle;font-size:10px;font-family:monospace","[% \"district\" %] — [% \"site_id\" %]",[44,543],{"x":531,"y":544,"width":545,"height":546,"rx":535,"fill":95,"stroke":547,"style":536},"98","216","106","#15803d",[68,549,552],{"x":550,"y":551,"style":120,"fill":66,"textAnchor":74},"308","156","map, controlled by atlas",[44,554],{"x":555,"y":544,"width":556,"height":546,"rx":535,"fill":114,"stroke":115,"style":536},"424","136",[68,558,561],{"x":559,"y":560,"style":120,"fill":66,"textAnchor":74},"492","146","related records",[68,563,565],{"x":559,"y":564,"style":120,"fill":66,"textAnchor":74},"166","table",[44,567],{"x":531,"y":80,"width":533,"height":534,"rx":535,"fill":568,"stroke":133,"style":536},"#eff3ff",[68,570,572],{"x":70,"y":571,"style":540,"fill":133,"textAnchor":74},"231","[% @atlas_featurenumber %] of [% @atlas_totalfeatures %]",[68,574,576],{"x":100,"y":575,"style":120,"fill":84,"textAnchor":74},"82","title label",[68,578,579],{"x":100,"y":551,"style":120,"fill":547,"textAnchor":74},"map item",[68,581,584],{"x":582,"y":583,"style":120,"fill":115,"textAnchor":74},"666","150","table item",[68,586,588],{"x":100,"y":587,"style":120,"fill":133,"textAnchor":74},"232","footer label",[44,590],{"x":180,"y":591,"width":146,"height":71,"rx":528,"fill":114,"stroke":115,"style":536},"272",[68,593,596],{"x":70,"y":594,"style":595,"fill":66,"textAnchor":74},"290","text-anchor:middle;font-size:10.5px;font-family:sans-serif","the [% %] delimiters are what make a label evaluate rather than print",[221,598,600],{"className":223,"code":599,"language":225,"meta":226,"style":226},"from qgis.core import QgsLayoutItemLabel\n\ntitle = layout.itemById(\"title\")\ntitle.setText('[% \"district\" || \\' — \\' || \"site_id\" %]')\n\nfooter = layout.itemById(\"footer\")\nfooter.setText(\n    \"[% 'Sheet ' || @atlas_featurenumber || ' of ' || @atlas_totalfeatures %]\"\n)\n",[228,601,602,613,617,632,651,655,669,674,679],{"__ignoreMap":226},[231,603,604,606,608,610],{"class":123,"line":233},[231,605,237],{"class":236},[231,607,241],{"class":240},[231,609,244],{"class":236},[231,611,612],{"class":240}," QgsLayoutItemLabel\n",[231,614,615],{"class":123,"line":250},[231,616,254],{"emptyLinePlaceholder":253},[231,618,619,622,624,627,630],{"class":123,"line":257},[231,620,621],{"class":240},"title ",[231,623,263],{"class":236},[231,625,626],{"class":240}," layout.itemById(",[231,628,629],{"class":280},"\"title\"",[231,631,284],{"class":240},[231,633,634,637,640,642,644,646,649],{"class":123,"line":269},[231,635,636],{"class":240},"title.setText(",[231,638,639],{"class":280},"'[% \"district\" || ",[231,641,352],{"class":304},[231,643,355],{"class":280},[231,645,352],{"class":304},[231,647,648],{"class":280}," || \"site_id\" %]'",[231,650,284],{"class":240},[231,652,653],{"class":123,"line":287},[231,654,254],{"emptyLinePlaceholder":253},[231,656,657,660,662,664,667],{"class":123,"line":310},[231,658,659],{"class":240},"footer ",[231,661,263],{"class":236},[231,663,626],{"class":240},[231,665,666],{"class":280},"\"footer\"",[231,668,284],{"class":240},[231,670,671],{"class":123,"line":315},[231,672,673],{"class":240},"footer.setText(\n",[231,675,676],{"class":123,"line":326},[231,677,678],{"class":280},"    \"[% 'Sheet ' || @atlas_featurenumber || ' of ' || @atlas_totalfeatures %]\"\n",[231,680,681],{"class":123,"line":337},[231,682,284],{"class":240},[14,684,685,687,688,691,692,695,696,699,700,703,704,707,708,711],{},[204,686,420],{}," Everything between ",[228,689,690],{},"[%"," and ",[228,693,694],{},"%]"," is evaluated as an expression and the rest is literal, so a label can mix fixed and dynamic text freely. Inside a label on an atlas page, bare field names again resolve against the coverage feature. Where a label needs a field from a ",[423,697,698],{},"different"," layer — a project-wide contact, say — an ",[228,701,702],{},"aggregate"," or an explicit ",[228,705,706],{},"attribute()"," call is required, because there is no implicit access. ",[228,709,710],{},"itemById"," finds items by the id set in their properties, which is worth assigning to every item a script will touch.",[193,713,715],{"id":714},"controlling-the-map-per-page","Controlling the map per page",[14,717,718],{},"The map item, not the atlas, decides how each page frames its feature, and the three modes give very different atlases.",[221,720,722],{"className":223,"code":721,"language":225,"meta":226,"style":226},"map_item = layout.itemById(\"main_map\")\nmap_item.setAtlasDriven(True)\nmap_item.setAtlasScalingMode(QgsLayoutItemMap.Auto)\nmap_item.setAtlasMargin(0.15)\n",[228,723,724,738,747,752],{"__ignoreMap":226},[231,725,726,729,731,733,736],{"class":123,"line":233},[231,727,728],{"class":240},"map_item ",[231,730,263],{"class":236},[231,732,626],{"class":240},[231,734,735],{"class":280},"\"main_map\"",[231,737,284],{"class":240},[231,739,740,743,745],{"class":123,"line":250},[231,741,742],{"class":240},"map_item.setAtlasDriven(",[231,744,332],{"class":304},[231,746,284],{"class":240},[231,748,749],{"class":123,"line":257},[231,750,751],{"class":240},"map_item.setAtlasScalingMode(QgsLayoutItemMap.Auto)\n",[231,753,754,757,760],{"class":123,"line":269},[231,755,756],{"class":240},"map_item.setAtlasMargin(",[231,758,759],{"class":304},"0.15",[231,761,284],{"class":240},[14,763,764,766,767,770,771,774,775,778],{},[204,765,420],{}," ",[228,768,769],{},"Auto"," fits each feature with a margin expressed as a fraction of the feature's size, so a small site and a large one each fill the frame — good for inspection sheets, bad for anything where pages are compared, because every page is at a different scale. ",[228,772,773],{},"Fixed"," keeps the map item's own scale and simply centres on each feature, which makes pages comparable and lets a large feature overflow. ",[228,776,777],{},"Predefined"," picks the smallest scale from the project's predefined list that fits the feature, which is the compromise most printed series want: comparable, round-numbered scales, and nothing clipped.",[14,780,781],{},"Because the scale can vary, any label quoting it should read it rather than state it:",[221,783,785],{"className":223,"code":784,"language":225,"meta":226,"style":226},"scale_label = layout.itemById(\"scale_note\")\nscale_label.setText(\"[% 'Scale 1:' || format_number(@map_scale, 0) %]\")\n",[228,786,787,801],{"__ignoreMap":226},[231,788,789,792,794,796,799],{"class":123,"line":233},[231,790,791],{"class":240},"scale_label ",[231,793,263],{"class":236},[231,795,626],{"class":240},[231,797,798],{"class":280},"\"scale_note\"",[231,800,284],{"class":240},[231,802,803,806,809],{"class":123,"line":250},[231,804,805],{"class":240},"scale_label.setText(",[231,807,808],{"class":280},"\"[% 'Scale 1:' || format_number(@map_scale, 0) %]\"",[231,810,284],{"class":240},[14,812,813,766,815,818,819,822,823,825],{},[204,814,420],{},[228,816,817],{},"@map_scale"," is evaluated in the context of the map the label is associated with, and a label with no associated map gets the layout's first one — which is right on a single-map page and wrong on a page with an inset. Setting the label's ",[228,820,821],{},"setLinkedMap()"," removes the ambiguity. A hard-coded scale on an ",[228,824,769],{}," atlas is one of the more embarrassing errors to reach print, because it is correct on the page the author checked.",[193,827,829],{"id":828},"spatial-filters-that-follow-the-page","Spatial filters that follow the page",[14,831,832,834],{},[228,833,174],{}," opens up filters that are about place rather than attributes, which is how a page shows only the features belonging to its own area.",[221,836,838],{"className":223,"code":837,"language":225,"meta":226,"style":226},"neighbours = project.mapLayersByName(\"neighbouring_sites\")[0]\nneighbours.setSubsetString(\"\")\n\nrule_filter = \"intersects($geometry, buffer(@atlas_geometry, 500))\"\n",[228,839,840,858,868,872],{"__ignoreMap":226},[231,841,842,845,847,849,852,854,856],{"class":123,"line":233},[231,843,844],{"class":240},"neighbours ",[231,846,263],{"class":236},[231,848,295],{"class":240},[231,850,851],{"class":280},"\"neighbouring_sites\"",[231,853,301],{"class":240},[231,855,46],{"class":304},[231,857,307],{"class":240},[231,859,860,863,866],{"class":123,"line":250},[231,861,862],{"class":240},"neighbours.setSubsetString(",[231,864,865],{"class":280},"\"\"",[231,867,284],{"class":240},[231,869,870],{"class":123,"line":257},[231,871,254],{"emptyLinePlaceholder":253},[231,873,874,877,879],{"class":123,"line":269},[231,875,876],{"class":240},"rule_filter ",[231,878,263],{"class":236},[231,880,881],{"class":280}," \"intersects($geometry, buffer(@atlas_geometry, 500))\"\n",[14,883,884,886,887,890],{},[204,885,420],{}," Used as a rule filter in a rule-based renderer, or as a label filter, this shows only features within 500 m of the current coverage feature — so the surrounding context appears without the whole layer being drawn. It is evaluated per page and per feature, so it is not free: on a large layer, restricting the layer's own extent through the map item is cheaper than an expression that tests every feature. Where the coverage features are polygons, ",[228,888,889],{},"intersects"," against the geometry directly is usually enough and needs no buffer.",[193,892,894],{"id":893},"testing-before-a-long-export","Testing before a long export",[221,896,898],{"className":223,"code":897,"language":225,"meta":226,"style":226},"atlas.beginRender()\nfor index in range(min(3, atlas.count())):\n    atlas.seekTo(index)\n    print(index, \"→\", atlas.currentFilename(), \"|\", atlas.nameForPage(index))\n    print(\"   title renders as:\", title.currentText())\natlas.endRender()\n",[228,899,900,905,931,936,956,968],{"__ignoreMap":226},[231,901,902],{"class":123,"line":233},[231,903,904],{"class":240},"atlas.beginRender()\n",[231,906,907,910,913,916,919,921,924,926,928],{"class":123,"line":250},[231,908,909],{"class":236},"for",[231,911,912],{"class":240}," index ",[231,914,915],{"class":236},"in",[231,917,918],{"class":304}," range",[231,920,483],{"class":240},[231,922,923],{"class":304},"min",[231,925,483],{"class":240},[231,927,535],{"class":304},[231,929,930],{"class":240},", atlas.count())):\n",[231,932,933],{"class":123,"line":257},[231,934,935],{"class":240},"    atlas.seekTo(index)\n",[231,937,938,941,944,947,950,953],{"class":123,"line":269},[231,939,940],{"class":304},"    print",[231,942,943],{"class":240},"(index, ",[231,945,946],{"class":280},"\"→\"",[231,948,949],{"class":240},", atlas.currentFilename(), ",[231,951,952],{"class":280},"\"|\"",[231,954,955],{"class":240},", atlas.nameForPage(index))\n",[231,957,958,960,962,965],{"class":123,"line":287},[231,959,940],{"class":304},[231,961,483],{"class":240},[231,963,964],{"class":280},"\"   title renders as:\"",[231,966,967],{"class":240},", title.currentText())\n",[231,969,970],{"class":123,"line":310},[231,971,972],{"class":240},"atlas.endRender()\n",[14,974,975,766,977,691,980,983,984,987,988,991,992,994,995,998],{},[204,976,420],{},[228,978,979],{},"beginRender",[228,981,982],{},"seekTo"," step the atlas without exporting anything, which is the cheapest possible way to check that expressions produce what you expect. ",[228,985,986],{},"currentFilename()"," shows the resolved filename for the current feature — the place where a duplicate name or an illegal character reveals itself, and duplicates matter because the exporter overwrites rather than warning. ",[228,989,990],{},"currentText()"," on a label returns the evaluated text rather than the expression source. Always pair ",[228,993,979],{}," with ",[228,996,997],{},"endRender","; leaving the atlas in render mode confuses the GUI and any subsequent export.",[14,1000,1001],{},"Watch for duplicates explicitly on a large coverage:",[221,1003,1005],{"className":223,"code":1004,"language":225,"meta":226,"style":226},"from collections import Counter\n\natlas.beginRender()\nnames = []\nfor index in range(atlas.count()):\n    atlas.seekTo(index)\n    names.append(atlas.currentFilename())\natlas.endRender()\n\nclashes = [name for name, n in Counter(names).items() if n > 1]\nprint(len(clashes), \"duplicate filenames\")\n",[228,1006,1007,1019,1023,1027,1037,1050,1054,1059,1063,1067,1101],{"__ignoreMap":226},[231,1008,1009,1011,1014,1016],{"class":123,"line":233},[231,1010,237],{"class":236},[231,1012,1013],{"class":240}," collections ",[231,1015,244],{"class":236},[231,1017,1018],{"class":240}," Counter\n",[231,1020,1021],{"class":123,"line":250},[231,1022,254],{"emptyLinePlaceholder":253},[231,1024,1025],{"class":123,"line":257},[231,1026,904],{"class":240},[231,1028,1029,1032,1034],{"class":123,"line":269},[231,1030,1031],{"class":240},"names ",[231,1033,263],{"class":236},[231,1035,1036],{"class":240}," []\n",[231,1038,1039,1041,1043,1045,1047],{"class":123,"line":287},[231,1040,909],{"class":236},[231,1042,912],{"class":240},[231,1044,915],{"class":236},[231,1046,918],{"class":304},[231,1048,1049],{"class":240},"(atlas.count()):\n",[231,1051,1052],{"class":123,"line":310},[231,1053,935],{"class":240},[231,1055,1056],{"class":123,"line":315},[231,1057,1058],{"class":240},"    names.append(atlas.currentFilename())\n",[231,1060,1061],{"class":123,"line":326},[231,1062,972],{"class":240},[231,1064,1065],{"class":123,"line":337},[231,1066,254],{"emptyLinePlaceholder":253},[231,1068,1069,1072,1074,1077,1079,1082,1084,1087,1090,1093,1096,1099],{"class":123,"line":343},[231,1070,1071],{"class":240},"clashes ",[231,1073,263],{"class":236},[231,1075,1076],{"class":240}," [name ",[231,1078,909],{"class":236},[231,1080,1081],{"class":240}," name, n ",[231,1083,915],{"class":236},[231,1085,1086],{"class":240}," Counter(names).items() ",[231,1088,1089],{"class":236},"if",[231,1091,1092],{"class":240}," n ",[231,1094,1095],{"class":236},">",[231,1097,1098],{"class":304}," 1",[231,1100,307],{"class":240},[231,1102,1103,1105,1107,1110,1113,1116],{"class":123,"line":365},[231,1104,480],{"class":304},[231,1106,483],{"class":240},[231,1108,1109],{"class":304},"len",[231,1111,1112],{"class":240},"(clashes), ",[231,1114,1115],{"class":280},"\"duplicate filenames\"",[231,1117,284],{"class":240},[14,1119,1120,1122,1123,1125],{},[204,1121,420],{}," A filename expression built from a field that is not unique produces pages that silently overwrite each other, and the result is an output folder with fewer files than the atlas had pages — a discrepancy nobody notices unless they count. Appending ",[228,1124,153],{}," to the expression guarantees uniqueness at the cost of a less tidy name, which is usually the right trade.",[193,1127,1129],{"id":1128},"page-names-and-why-they-are-worth-setting","Page names, and why they are worth setting",[14,1131,1132,1133,1135],{},"The page name expression looks cosmetic and is not. It supplies the bookmark label in a combined PDF, the entry in the layout's page-name list, and the value of ",[228,1134,167],{}," for any label that wants to print it. A four-hundred-page PDF whose bookmarks all read \"Page 12\" is considerably less useful than one whose bookmarks name the district and site.",[221,1137,1139],{"className":223,"code":1138,"language":225,"meta":226,"style":226},"atlas.setPageNameExpression(\n    '\"district\" || \\' \\' || lpad(@atlas_featurenumber, 3, \\'0\\')'\n)\n",[228,1140,1141,1146,1168],{"__ignoreMap":226},[231,1142,1143],{"class":123,"line":233},[231,1144,1145],{"class":240},"atlas.setPageNameExpression(\n",[231,1147,1148,1151,1153,1156,1159,1161,1163,1165],{"class":123,"line":250},[231,1149,1150],{"class":280},"    '\"district\" || ",[231,1152,352],{"class":304},[231,1154,1155],{"class":304}," \\'",[231,1157,1158],{"class":280}," || lpad(@atlas_featurenumber, 3, ",[231,1160,352],{"class":304},[231,1162,46],{"class":280},[231,1164,352],{"class":304},[231,1166,1167],{"class":280},")'\n",[231,1169,1170],{"class":123,"line":257},[231,1171,284],{"class":240},[14,1173,1174,1176,1177,1180],{},[204,1175,420],{}," Combining a meaningful field with a zero-padded sequence number gives names that sort correctly as text and remain unique even where the field does not — ",[228,1178,1179],{},"lpad"," is what makes 9 sort before 10 rather than after it. Keeping the page name and the filename expression consistent, so a bookmark and a file refer to the same thing by the same words, saves everyone downstream a translation step.",[14,1182,1183],{},"One caution: the page name is evaluated once per feature and is not re-evaluated if the coverage changes underneath a rendering atlas, so a script that edits the coverage layer mid-export produces names that no longer match their pages. Finish the export, then edit.",[193,1185,1187],{"id":1186},"qgis-version-compatibility","QGIS version compatibility",[14,1189,1190,1193,1194,1197,1198,1200,1201,1200,1203,1200,1205,691,1207,1209,1210,1213],{},[228,1191,1192],{},"QgsLayoutAtlas"," and the ",[228,1195,1196],{},"layout.atlas()"," accessor arrived with the layout rewrite in QGIS 3.0 and are unchanged in shape since. The atlas variables ",[228,1199,143],{},", ",[228,1202,153],{},[228,1204,160],{},[228,1206,167],{},[228,1208,174],{}," have been present throughout 3.x. ",[228,1211,1212],{},"nameForPage"," was added in 3.4.",[193,1215,1217],{"id":1216},"troubleshooting","Troubleshooting",[198,1219,1220,1231,1240,1246,1252,1261],{},[201,1221,1222,1225,1226,691,1228,1230],{},[204,1223,1224],{},"Labels print their own expression."," Missing ",[228,1227,690],{},[228,1229,694],{}," delimiters.",[201,1232,1233,766,1236,1239],{},[204,1234,1235],{},"The filter has no effect.",[228,1237,1238],{},"setFilterFeatures(True)"," was not called.",[201,1241,1242,1245],{},[204,1243,1244],{},"Fewer files than pages."," Duplicate filenames overwriting each other.",[201,1247,1248,1251],{},[204,1249,1250],{},"Pages come out in a different order each export."," Sorting is off, so provider order applies.",[201,1253,1254,1257,1258,215],{},[204,1255,1256],{},"A field name is not found in a label."," The item's layer context is not the coverage layer; use ",[228,1259,1260],{},"attribute(@atlas_feature, 'field')",[201,1262,1263,1266],{},[204,1264,1265],{},"The export fails on the first page."," The filename expression contains a path separator or an extension.",[193,1268,1270],{"id":1269},"conclusion","Conclusion",[14,1272,1273,1274,1276,1277,1279,1280,1282],{},"Set the coverage, the sort, the filter and the two expressions from Python, then step three pages with ",[228,1275,979],{},"\u002F",[228,1278,982],{}," and read the resolved filenames and labels before exporting anything. Checking ",[228,1281,497],{}," and the duplicate filenames takes seconds and catches the two mistakes that otherwise only appear after a four-hundred-page run.",[193,1284,1286],{"id":1285},"frequently-asked-questions","Frequently Asked Questions",[14,1288,1289,1292],{},[204,1290,1291],{},"Can the map's scale differ per page?","\nYes — set the map item's atlas driving mode to a fixed scale, a margin around the feature, or a predefined scale list. The predefined-scale mode is what gives a consistent set of round scales across a series.",[14,1294,1295,1298,1299,1301,1302,1304],{},[204,1296,1297],{},"How do I show only the current feature's labels?","\nFilter the labelled layer with an expression referencing ",[228,1300,143],{},", or use ",[228,1303,174],{}," in a spatial condition. Both are evaluated per page.",[14,1306,1307,1310,1311,215],{},[204,1308,1309],{},"Can I export each page to a separate PDF?","\nYes — the exporter has a per-feature mode, and the filename expression names each file. A single combined PDF is the alternative, covered in ",[21,1312,1314],{"href":1313},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fexport-atlas-pages-to-individual-pdfs-pyqgis\u002F","exporting atlas pages to individual PDFs",[14,1316,1317,1320,1321,215],{},[204,1318,1319],{},"Does an atlas work with a table item?","\nYes, and it is the point of the relation-children table source — see ",[21,1322,1324],{"href":1323},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-attribute-table-to-layout-pyqgis\u002F","adding an attribute table to a layout",[193,1326,1328],{"id":1327},"related","Related",[198,1330,1331,1337,1342,1348,1353],{},[201,1332,1333,1336],{},[21,1334,1335],{"href":23},"Automating Atlas Map Series in PyQGIS"," — the guide this recipe belongs to",[201,1338,1339],{},[21,1340,1341],{"href":213},"Configure an Atlas Coverage Layer in PyQGIS",[201,1343,1344],{},[21,1345,1347],{"href":1346},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fgenerate-atlas-pdf-pyqgis\u002F","Generate an Atlas PDF in PyQGIS",[201,1349,1350],{},[21,1351,1352],{"href":1323},"Add an Attribute Table to a Layout in PyQGIS",[201,1354,1355],{},[21,1356,1358],{"href":1357},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","Evaluate a QGIS Expression in PyQGIS",[1360,1361,1362],"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);}",{"title":226,"searchDepth":250,"depth":250,"links":1364},[1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377],{"id":195,"depth":250,"text":196},{"id":218,"depth":250,"text":219},{"id":436,"depth":250,"text":437},{"id":504,"depth":250,"text":505},{"id":714,"depth":250,"text":715},{"id":828,"depth":250,"text":829},{"id":893,"depth":250,"text":894},{"id":1128,"depth":250,"text":1129},{"id":1186,"depth":250,"text":1187},{"id":1216,"depth":250,"text":1217},{"id":1269,"depth":250,"text":1270},{"id":1285,"depth":250,"text":1286},{"id":1327,"depth":250,"text":1328},"Drive atlas titles, filenames, page names and filters from expressions in PyQGIS — the atlas variables, evaluating them per feature, and testing before a 400-page export.","md",{"slug":1381,"type":1382,"breadcrumb":1383,"datePublished":1384,"dateModified":1384},"atlas-expressions-and-dynamic-text-pyqgis","article","Atlas Expressions & Dynamic Text","2026-09-05","\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fatlas-expressions-and-dynamic-text-pyqgis",{"title":5,"description":1378},"spatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fatlas-expressions-and-dynamic-text-pyqgis\u002Findex","HCR3Cn9g4U9PLpqLvjV-mxBc1vAyUzGakvvCtvSdd9g",1788563852391]