[{"data":1,"prerenderedAt":1657},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis":3},{"id":4,"title":5,"body":6,"description":1650,"extension":1651,"meta":1652,"navigation":310,"path":1653,"seo":1654,"stem":1655,"__hash__":1656},"docs\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis\u002Findex.md","Add Rule-Based Labels in PyQGIS",{"type":7,"value":8,"toc":1635},"minimark",[9,13,32,40,51,56,92,96,117,225,229,235,519,540,544,558,865,893,897,903,1014,1034,1038,1041,1131,1139,1143,1154,1295,1300,1304,1307,1373,1382,1386,1395,1484,1490,1494,1508,1531,1551,1560,1564,1577,1581,1590,1606,1612,1616,1631],[10,11,5],"h1",{"id":12},"add-rule-based-labels-in-pyqgis",[14,15,16,17,21,22,25,26,31],"p",{},"Simple labeling applies one style to every feature. Real maps need more nuance: label motorways at country scale, add minor streets only when the user zooms in, and skip footpaths entirely. ",[18,19,20],"code",{},"QgsRuleBasedLabeling"," gives you exactly this by organizing labeling into a tree of rules, each carrying its own filter expression, scale range, and ",[18,23,24],{},"QgsPalLayerSettings",". This task page, part of the ",[27,28,30],"a",{"href":29},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002F","Labeling & Annotations in PyQGIS"," cluster, walks through building a road-network labeling tree from a root rule down to scale-aware children.",[14,33,34,35,39],{},"The pattern mirrors rule-based ",[36,37,38],"em",{},"rendering"," — if you have set symbol styles by class, the labeling API will feel familiar. The difference is that each rule wraps a label settings object instead of a symbol.",[14,41,42,43,46,47,50],{},"Why reach for rules at all when data-defined ",[18,44,45],{},"Show"," expressions can hide labels conditionally? Because rules give each class its ",[36,48,49],{},"own"," full settings object. A motorway rule can use an 11-point bold serif at every scale while a residential rule uses 8-point sans only when zoomed in — different fonts, sizes, colors, placements, and scale ranges, all coexisting on one layer. A single set of data-defined properties cannot vary placement or font family per feature with anything like that clarity. Rules also read like a legend, which makes them maintainable months later.",[52,53,55],"h2",{"id":54},"prerequisites","Prerequisites",[57,58,59,67,82,89],"ul",{},[60,61,62,66],"li",{},[63,64,65],"strong",{},"QGIS 3.34 LTR"," (Python 3.12). The API shown is stable back to 3.28 LTR.",[60,68,69,70,73,74,77,78,81],{},"A line layer of roads with a classification field (commonly ",[18,71,72],{},"highway",", ",[18,75,76],{},"fclass",", or ",[18,79,80],{},"road_class",") — OpenStreetMap road extracts work perfectly.",[60,83,84,85,88],{},"A configured ",[18,86,87],{},"QgsTextFormat"," to reuse across rules (see the parent cluster for building one).",[60,90,91],{},"Code running in the QGIS Python Console or a standalone script with an initialized application.",[52,93,95],{"id":94},"how-rule-based-labeling-is-structured","How Rule-Based Labeling Is Structured",[14,97,98,99,101,102,105,106,109,110,112,113,116],{},"A ",[18,100,20],{}," is built from a single ",[63,103,104],{},"root rule",". The root usually carries no settings of its own; it is a container. You append ",[63,107,108],{},"child rules"," to it, and each child holds a ",[18,111,24],{},", an optional filter expression, and an optional scale range. At render time the engine walks the tree and applies every child whose filter matches the feature ",[36,114,115],{},"and"," whose scale range includes the current map scale.",[118,119,124,125,124,129,124,133,124,140,124,150,124,157,124,163,124,170,124,172,124,175,124,181,124,186,124,190,124,194,124,199,124,203,124,206,124,209,124,212,124,216,124,219,124,222],"svg",{"viewBox":120,"role":121,"ariaLabel":122,"xmlns":123},"0 0 720 300","img","Tree diagram of a rule-based labeling root rule with three scale-aware child rules","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[126,127,128],"title",{},"Rule-based labeling tree",[130,131,132],"desc",{},"A root rule branches to three children: motorways visible at all scales, secondary roads from 1:250000, and residential streets only below 1:25000.",[134,135],"rect",{"x":136,"y":136,"width":137,"height":138,"fill":139},"0","720","300","#f6f3ea",[134,141],{"x":142,"y":143,"width":144,"height":145,"rx":146,"fill":147,"stroke":148,"style":149},"270","24","180","50","8","#fffdf7","#17211d","stroke-width:2.5",[151,152,156],"text",{"x":153,"y":154,"fill":148,"style":155},"360","46","text-anchor:middle;font-family:sans-serif;font-size:14px;font-weight:bold","Root rule",[151,158,162],{"x":153,"y":159,"fill":160,"style":161},"64","#2f3b35","text-anchor:middle;font-family:sans-serif;font-size:11px","container, no settings",[164,165],"line",{"x1":153,"y1":166,"x2":167,"y2":168,"stroke":160,"style":169},"74","130","150","stroke-width:2",[164,171],{"x1":153,"y1":166,"x2":153,"y2":168,"stroke":160,"style":169},[164,173],{"x1":153,"y1":166,"x2":174,"y2":168,"stroke":160,"style":169},"590",[134,176],{"x":177,"y":168,"width":178,"height":179,"rx":146,"fill":147,"stroke":180,"style":149},"30","200","110","#0f766e",[151,182,185],{"x":167,"y":183,"fill":148,"style":184},"174","text-anchor:middle;font-family:sans-serif;font-size:13px;font-weight:bold","Motorways",[151,187,189],{"x":167,"y":188,"fill":160,"style":161},"196","filter: highway =",[151,191,193],{"x":167,"y":192,"fill":160,"style":161},"210","'motorway'",[151,195,198],{"x":167,"y":196,"fill":197,"style":161},"236","#b45309","all scales",[134,200],{"x":201,"y":168,"width":178,"height":179,"rx":146,"fill":147,"stroke":202,"style":149},"260","#2563eb",[151,204,205],{"x":153,"y":183,"fill":148,"style":184},"Secondary",[151,207,208],{"x":153,"y":188,"fill":160,"style":161},"filter: secondary",[151,210,211],{"x":153,"y":196,"fill":197,"style":161},"≤ 1:250000",[134,213],{"x":214,"y":168,"width":178,"height":179,"rx":146,"fill":147,"stroke":215,"style":149},"490","#15803d",[151,217,218],{"x":174,"y":183,"fill":148,"style":184},"Residential",[151,220,221],{"x":174,"y":188,"fill":160,"style":161},"filter: residential",[151,223,224],{"x":174,"y":196,"fill":197,"style":161},"≤ 1:25000",[52,226,228],{"id":227},"step-1-build-a-reusable-label-settings-factory","Step 1: Build a Reusable Label Settings Factory",[14,230,231,232,234],{},"Each rule needs its own ",[18,233,24],{},". Rather than duplicate configuration, write a small factory that stamps out settings from a shared text format, varying only what changes per rule.",[236,237,242],"pre",{"className":238,"code":239,"language":240,"meta":241,"style":241},"language-python shiki shiki-themes github-dark","from qgis.PyQt.QtGui import QColor, QFont\nfrom qgis.core import (\n    QgsTextFormat,\n    QgsTextBufferSettings,\n    QgsPalLayerSettings,\n    QgsUnitTypes,\n)\n\n\ndef make_text_format(point_size, color):\n    fmt = QgsTextFormat()\n    fmt.setFont(QFont(\"Noto Sans\", point_size))\n    fmt.setSize(point_size)\n    fmt.setSizeUnit(QgsUnitTypes.RenderPoints)\n    fmt.setColor(QColor(color))\n    buffer = QgsTextBufferSettings()\n    buffer.setEnabled(True)\n    buffer.setSize(1.0)\n    buffer.setSizeUnit(QgsUnitTypes.RenderMillimeters)\n    buffer.setColor(QColor(\"#ffffff\"))\n    fmt.setBuffer(buffer)\n    return fmt\n\n\ndef road_label_settings(field, point_size, color):\n    settings = QgsPalLayerSettings()\n    settings.fieldName = field\n    settings.isExpression = False\n    settings.placement = QgsPalLayerSettings.Curved\n    settings.setFormat(make_text_format(point_size, color))\n    return settings\n","python","",[18,243,244,262,275,281,287,293,299,305,312,317,330,342,355,361,367,373,384,396,407,413,425,431,440,445,450,461,472,483,494,505,511],{"__ignoreMap":241},[245,246,248,252,256,259],"span",{"class":164,"line":247},1,[245,249,251],{"class":250},"snl16","from",[245,253,255],{"class":254},"s95oV"," qgis.PyQt.QtGui ",[245,257,258],{"class":250},"import",[245,260,261],{"class":254}," QColor, QFont\n",[245,263,265,267,270,272],{"class":164,"line":264},2,[245,266,251],{"class":250},[245,268,269],{"class":254}," qgis.core ",[245,271,258],{"class":250},[245,273,274],{"class":254}," (\n",[245,276,278],{"class":164,"line":277},3,[245,279,280],{"class":254},"    QgsTextFormat,\n",[245,282,284],{"class":164,"line":283},4,[245,285,286],{"class":254},"    QgsTextBufferSettings,\n",[245,288,290],{"class":164,"line":289},5,[245,291,292],{"class":254},"    QgsPalLayerSettings,\n",[245,294,296],{"class":164,"line":295},6,[245,297,298],{"class":254},"    QgsUnitTypes,\n",[245,300,302],{"class":164,"line":301},7,[245,303,304],{"class":254},")\n",[245,306,308],{"class":164,"line":307},8,[245,309,311],{"emptyLinePlaceholder":310},true,"\n",[245,313,315],{"class":164,"line":314},9,[245,316,311],{"emptyLinePlaceholder":310},[245,318,320,323,327],{"class":164,"line":319},10,[245,321,322],{"class":250},"def",[245,324,326],{"class":325},"svObZ"," make_text_format",[245,328,329],{"class":254},"(point_size, color):\n",[245,331,333,336,339],{"class":164,"line":332},11,[245,334,335],{"class":254},"    fmt ",[245,337,338],{"class":250},"=",[245,340,341],{"class":254}," QgsTextFormat()\n",[245,343,345,348,352],{"class":164,"line":344},12,[245,346,347],{"class":254},"    fmt.setFont(QFont(",[245,349,351],{"class":350},"sU2Wk","\"Noto Sans\"",[245,353,354],{"class":254},", point_size))\n",[245,356,358],{"class":164,"line":357},13,[245,359,360],{"class":254},"    fmt.setSize(point_size)\n",[245,362,364],{"class":164,"line":363},14,[245,365,366],{"class":254},"    fmt.setSizeUnit(QgsUnitTypes.RenderPoints)\n",[245,368,370],{"class":164,"line":369},15,[245,371,372],{"class":254},"    fmt.setColor(QColor(color))\n",[245,374,376,379,381],{"class":164,"line":375},16,[245,377,378],{"class":254},"    buffer ",[245,380,338],{"class":250},[245,382,383],{"class":254}," QgsTextBufferSettings()\n",[245,385,387,390,394],{"class":164,"line":386},17,[245,388,389],{"class":254},"    buffer.setEnabled(",[245,391,393],{"class":392},"sDLfK","True",[245,395,304],{"class":254},[245,397,399,402,405],{"class":164,"line":398},18,[245,400,401],{"class":254},"    buffer.setSize(",[245,403,404],{"class":392},"1.0",[245,406,304],{"class":254},[245,408,410],{"class":164,"line":409},19,[245,411,412],{"class":254},"    buffer.setSizeUnit(QgsUnitTypes.RenderMillimeters)\n",[245,414,416,419,422],{"class":164,"line":415},20,[245,417,418],{"class":254},"    buffer.setColor(QColor(",[245,420,421],{"class":350},"\"#ffffff\"",[245,423,424],{"class":254},"))\n",[245,426,428],{"class":164,"line":427},21,[245,429,430],{"class":254},"    fmt.setBuffer(buffer)\n",[245,432,434,437],{"class":164,"line":433},22,[245,435,436],{"class":250},"    return",[245,438,439],{"class":254}," fmt\n",[245,441,443],{"class":164,"line":442},23,[245,444,311],{"emptyLinePlaceholder":310},[245,446,448],{"class":164,"line":447},24,[245,449,311],{"emptyLinePlaceholder":310},[245,451,453,455,458],{"class":164,"line":452},25,[245,454,322],{"class":250},[245,456,457],{"class":325}," road_label_settings",[245,459,460],{"class":254},"(field, point_size, color):\n",[245,462,464,467,469],{"class":164,"line":463},26,[245,465,466],{"class":254},"    settings ",[245,468,338],{"class":250},[245,470,471],{"class":254}," QgsPalLayerSettings()\n",[245,473,475,478,480],{"class":164,"line":474},27,[245,476,477],{"class":254},"    settings.fieldName ",[245,479,338],{"class":250},[245,481,482],{"class":254}," field\n",[245,484,486,489,491],{"class":164,"line":485},28,[245,487,488],{"class":254},"    settings.isExpression ",[245,490,338],{"class":250},[245,492,493],{"class":392}," False\n",[245,495,497,500,502],{"class":164,"line":496},29,[245,498,499],{"class":254},"    settings.placement ",[245,501,338],{"class":250},[245,503,504],{"class":254}," QgsPalLayerSettings.Curved\n",[245,506,508],{"class":164,"line":507},30,[245,509,510],{"class":254},"    settings.setFormat(make_text_format(point_size, color))\n",[245,512,514,516],{"class":164,"line":513},31,[245,515,436],{"class":250},[245,517,518],{"class":254}," settings\n",[14,520,521,524,525,528,529,532,533,535,536,539],{},[63,522,523],{},"Breakdown:"," ",[18,526,527],{},"make_text_format"," centralizes typography so every rule shares the same white halo and font family while letting size and color vary. ",[18,530,531],{},"road_label_settings"," returns a fresh ",[18,534,24],{}," per call — important, because rules must not share a single mutable settings object. ",[18,537,538],{},"Curved"," placement bends road names along their geometry, which is what readers expect from a street map.",[52,541,543],{"id":542},"step-2-create-the-root-rule-and-add-children","Step 2: Create the Root Rule and Add Children",[14,545,546,549,550,553,554,557],{},[18,547,548],{},"QgsRuleBasedLabeling.Rule"," is the node type. Construct the root with ",[18,551,552],{},"None"," (it holds no settings), then construct child rules wrapping your settings objects, give each a filter expression and scale range, and append them with ",[18,555,556],{},"appendChild",".",[236,559,561],{"className":238,"code":560,"language":240,"meta":241,"style":241},"from qgis.core import QgsRuleBasedLabeling\n\n# Root rule is a container; pass None for its settings.\nroot = QgsRuleBasedLabeling.Rule(None)\n\n# --- Child 1: motorways, labeled at every scale ---\nmotorway = QgsRuleBasedLabeling.Rule(\n    road_label_settings(\"name\", 11, \"#7c2d12\")\n)\nmotorway.setFilterExpression(\"\\\"highway\\\" = 'motorway'\")\nmotorway.setDescription(\"Motorways\")\nroot.appendChild(motorway)\n\n# --- Child 2: secondary roads, only from 1:250000 inward ---\nsecondary = QgsRuleBasedLabeling.Rule(\n    road_label_settings(\"name\", 9, \"#1f2937\")\n)\nsecondary.setFilterExpression(\"\\\"highway\\\" = 'secondary'\")\nsecondary.setMaximumScale(250000)   # not shown when zoomed out beyond this\nsecondary.setMinimumScale(1)\nsecondary.setDescription(\"Secondary roads\")\nroot.appendChild(secondary)\n\n# --- Child 3: residential streets, only below 1:25000 ---\nresidential = QgsRuleBasedLabeling.Rule(\n    road_label_settings(\"name\", 8, \"#374151\")\n)\nresidential.setFilterExpression(\"\\\"highway\\\" = 'residential'\")\nresidential.setMaximumScale(25000)\nresidential.setMinimumScale(1)\nresidential.setDescription(\"Residential streets\")\nroot.appendChild(residential)\n",[18,562,563,574,578,584,598,602,607,617,637,641,661,671,676,680,685,694,712,716,734,748,758,768,773,777,782,791,808,812,830,840,849,859],{"__ignoreMap":241},[245,564,565,567,569,571],{"class":164,"line":247},[245,566,251],{"class":250},[245,568,269],{"class":254},[245,570,258],{"class":250},[245,572,573],{"class":254}," QgsRuleBasedLabeling\n",[245,575,576],{"class":164,"line":264},[245,577,311],{"emptyLinePlaceholder":310},[245,579,580],{"class":164,"line":277},[245,581,583],{"class":582},"sAwPA","# Root rule is a container; pass None for its settings.\n",[245,585,586,589,591,594,596],{"class":164,"line":283},[245,587,588],{"class":254},"root ",[245,590,338],{"class":250},[245,592,593],{"class":254}," QgsRuleBasedLabeling.Rule(",[245,595,552],{"class":392},[245,597,304],{"class":254},[245,599,600],{"class":164,"line":289},[245,601,311],{"emptyLinePlaceholder":310},[245,603,604],{"class":164,"line":295},[245,605,606],{"class":582},"# --- Child 1: motorways, labeled at every scale ---\n",[245,608,609,612,614],{"class":164,"line":301},[245,610,611],{"class":254},"motorway ",[245,613,338],{"class":250},[245,615,616],{"class":254}," QgsRuleBasedLabeling.Rule(\n",[245,618,619,622,625,627,630,632,635],{"class":164,"line":307},[245,620,621],{"class":254},"    road_label_settings(",[245,623,624],{"class":350},"\"name\"",[245,626,73],{"class":254},[245,628,629],{"class":392},"11",[245,631,73],{"class":254},[245,633,634],{"class":350},"\"#7c2d12\"",[245,636,304],{"class":254},[245,638,639],{"class":164,"line":314},[245,640,304],{"class":254},[245,642,643,646,649,652,654,656,659],{"class":164,"line":319},[245,644,645],{"class":254},"motorway.setFilterExpression(",[245,647,648],{"class":350},"\"",[245,650,651],{"class":392},"\\\"",[245,653,72],{"class":350},[245,655,651],{"class":392},[245,657,658],{"class":350}," = 'motorway'\"",[245,660,304],{"class":254},[245,662,663,666,669],{"class":164,"line":332},[245,664,665],{"class":254},"motorway.setDescription(",[245,667,668],{"class":350},"\"Motorways\"",[245,670,304],{"class":254},[245,672,673],{"class":164,"line":344},[245,674,675],{"class":254},"root.appendChild(motorway)\n",[245,677,678],{"class":164,"line":357},[245,679,311],{"emptyLinePlaceholder":310},[245,681,682],{"class":164,"line":363},[245,683,684],{"class":582},"# --- Child 2: secondary roads, only from 1:250000 inward ---\n",[245,686,687,690,692],{"class":164,"line":369},[245,688,689],{"class":254},"secondary ",[245,691,338],{"class":250},[245,693,616],{"class":254},[245,695,696,698,700,702,705,707,710],{"class":164,"line":375},[245,697,621],{"class":254},[245,699,624],{"class":350},[245,701,73],{"class":254},[245,703,704],{"class":392},"9",[245,706,73],{"class":254},[245,708,709],{"class":350},"\"#1f2937\"",[245,711,304],{"class":254},[245,713,714],{"class":164,"line":386},[245,715,304],{"class":254},[245,717,718,721,723,725,727,729,732],{"class":164,"line":398},[245,719,720],{"class":254},"secondary.setFilterExpression(",[245,722,648],{"class":350},[245,724,651],{"class":392},[245,726,72],{"class":350},[245,728,651],{"class":392},[245,730,731],{"class":350}," = 'secondary'\"",[245,733,304],{"class":254},[245,735,736,739,742,745],{"class":164,"line":409},[245,737,738],{"class":254},"secondary.setMaximumScale(",[245,740,741],{"class":392},"250000",[245,743,744],{"class":254},")   ",[245,746,747],{"class":582},"# not shown when zoomed out beyond this\n",[245,749,750,753,756],{"class":164,"line":415},[245,751,752],{"class":254},"secondary.setMinimumScale(",[245,754,755],{"class":392},"1",[245,757,304],{"class":254},[245,759,760,763,766],{"class":164,"line":427},[245,761,762],{"class":254},"secondary.setDescription(",[245,764,765],{"class":350},"\"Secondary roads\"",[245,767,304],{"class":254},[245,769,770],{"class":164,"line":433},[245,771,772],{"class":254},"root.appendChild(secondary)\n",[245,774,775],{"class":164,"line":442},[245,776,311],{"emptyLinePlaceholder":310},[245,778,779],{"class":164,"line":447},[245,780,781],{"class":582},"# --- Child 3: residential streets, only below 1:25000 ---\n",[245,783,784,787,789],{"class":164,"line":452},[245,785,786],{"class":254},"residential ",[245,788,338],{"class":250},[245,790,616],{"class":254},[245,792,793,795,797,799,801,803,806],{"class":164,"line":463},[245,794,621],{"class":254},[245,796,624],{"class":350},[245,798,73],{"class":254},[245,800,146],{"class":392},[245,802,73],{"class":254},[245,804,805],{"class":350},"\"#374151\"",[245,807,304],{"class":254},[245,809,810],{"class":164,"line":474},[245,811,304],{"class":254},[245,813,814,817,819,821,823,825,828],{"class":164,"line":485},[245,815,816],{"class":254},"residential.setFilterExpression(",[245,818,648],{"class":350},[245,820,651],{"class":392},[245,822,72],{"class":350},[245,824,651],{"class":392},[245,826,827],{"class":350}," = 'residential'\"",[245,829,304],{"class":254},[245,831,832,835,838],{"class":164,"line":496},[245,833,834],{"class":254},"residential.setMaximumScale(",[245,836,837],{"class":392},"25000",[245,839,304],{"class":254},[245,841,842,845,847],{"class":164,"line":507},[245,843,844],{"class":254},"residential.setMinimumScale(",[245,846,755],{"class":392},[245,848,304],{"class":254},[245,850,851,854,857],{"class":164,"line":513},[245,852,853],{"class":254},"residential.setDescription(",[245,855,856],{"class":350},"\"Residential streets\"",[245,858,304],{"class":254},[245,860,862],{"class":164,"line":861},32,[245,863,864],{"class":254},"root.appendChild(residential)\n",[14,866,867,869,870,873,874,877,878,881,882,885,886,889,890,892],{},[63,868,523],{}," Each child wraps an independent settings object from the factory. ",[18,871,872],{},"setFilterExpression"," restricts the rule to one road class. The scale methods are the part that trips people up: in QGIS, ",[18,875,876],{},"setMaximumScale"," is the ",[36,879,880],{},"largest denominator"," at which the rule is active — counterintuitively, ",[18,883,884],{},"setMaximumScale(250000)"," means \"do not show when zoomed out past 1:250000.\" A ",[18,887,888],{},"minimumScale"," of ",[18,891,755],{}," keeps the rule active at all closer zooms. Leaving both unset (as on the motorway rule) labels at every scale.",[52,894,896],{"id":895},"step-3-attach-the-labeling-and-refresh","Step 3: Attach the Labeling and Refresh",[14,898,899,900,902],{},"Wrap the root in ",[18,901,20],{},", install it, enable labels, and repaint — the same three-call sequence every labeling type requires.",[236,904,906],{"className":238,"code":905,"language":240,"meta":241,"style":241},"from qgis.core import QgsRuleBasedLabeling, QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"roads\")[0]\n\nlabeling = QgsRuleBasedLabeling(root)\nlayer.setLabeling(labeling)\nlayer.setLabelsEnabled(True)\nlayer.triggerRepaint()\n\n# Optional: confirm the tree built correctly\nfor child in layer.labeling().rootRule().children():\n    print(child.description(), \"->\", child.filterExpression())\n",[18,907,908,919,923,944,948,958,963,972,977,981,986,1000],{"__ignoreMap":241},[245,909,910,912,914,916],{"class":164,"line":247},[245,911,251],{"class":250},[245,913,269],{"class":254},[245,915,258],{"class":250},[245,917,918],{"class":254}," QgsRuleBasedLabeling, QgsProject\n",[245,920,921],{"class":164,"line":264},[245,922,311],{"emptyLinePlaceholder":310},[245,924,925,928,930,933,936,939,941],{"class":164,"line":277},[245,926,927],{"class":254},"layer ",[245,929,338],{"class":250},[245,931,932],{"class":254}," QgsProject.instance().mapLayersByName(",[245,934,935],{"class":350},"\"roads\"",[245,937,938],{"class":254},")[",[245,940,136],{"class":392},[245,942,943],{"class":254},"]\n",[245,945,946],{"class":164,"line":283},[245,947,311],{"emptyLinePlaceholder":310},[245,949,950,953,955],{"class":164,"line":289},[245,951,952],{"class":254},"labeling ",[245,954,338],{"class":250},[245,956,957],{"class":254}," QgsRuleBasedLabeling(root)\n",[245,959,960],{"class":164,"line":295},[245,961,962],{"class":254},"layer.setLabeling(labeling)\n",[245,964,965,968,970],{"class":164,"line":301},[245,966,967],{"class":254},"layer.setLabelsEnabled(",[245,969,393],{"class":392},[245,971,304],{"class":254},[245,973,974],{"class":164,"line":307},[245,975,976],{"class":254},"layer.triggerRepaint()\n",[245,978,979],{"class":164,"line":314},[245,980,311],{"emptyLinePlaceholder":310},[245,982,983],{"class":164,"line":319},[245,984,985],{"class":582},"# Optional: confirm the tree built correctly\n",[245,987,988,991,994,997],{"class":164,"line":332},[245,989,990],{"class":250},"for",[245,992,993],{"class":254}," child ",[245,995,996],{"class":250},"in",[245,998,999],{"class":254}," layer.labeling().rootRule().children():\n",[245,1001,1002,1005,1008,1011],{"class":164,"line":344},[245,1003,1004],{"class":392},"    print",[245,1006,1007],{"class":254},"(child.description(), ",[245,1009,1010],{"class":350},"\"->\"",[245,1012,1013],{"class":254},", child.filterExpression())\n",[14,1015,1016,524,1018,1021,1022,1025,1026,1029,1030,557],{},[63,1017,523],{},[18,1019,1020],{},"QgsRuleBasedLabeling(root)"," takes ownership of the rule tree. ",[18,1023,1024],{},"setLabelsEnabled(True)"," is mandatory — without it the rules exist but draw nothing. The verification loop reads the rules back from ",[18,1027,1028],{},"layer.labeling().rootRule().children()",", a handy sanity check when a script builds rules dynamically. Because rule filters use the same expression engine as everywhere else in QGIS, you can reference any field or function you would use when you ",[27,1031,1033],{"href":1032},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fset-vector-layer-symbol-color-pyqgis\u002F","set a vector layer's symbol color in PyQGIS",[52,1035,1037],{"id":1036},"combining-filters-with-other-attributes","Combining Filters with Other Attributes",[14,1039,1040],{},"Rules are not limited to a single field. Combine class and importance so, for example, only named major roads are labeled at small scales:",[236,1042,1044],{"className":238,"code":1043,"language":240,"meta":241,"style":241},"major = QgsRuleBasedLabeling.Rule(road_label_settings(\"name\", 10, \"#7c2d12\"))\nmajor.setFilterExpression(\n    \"\\\"highway\\\" IN ('motorway', 'trunk', 'primary') \"\n    \"AND \\\"name\\\" IS NOT NULL AND length(\\\"name\\\") > 0\"\n)\nmajor.setMaximumScale(500000)\nroot.appendChild(major)\n",[18,1045,1046,1069,1074,1088,1112,1116,1126],{"__ignoreMap":241},[245,1047,1048,1051,1053,1056,1058,1060,1063,1065,1067],{"class":164,"line":247},[245,1049,1050],{"class":254},"major ",[245,1052,338],{"class":250},[245,1054,1055],{"class":254}," QgsRuleBasedLabeling.Rule(road_label_settings(",[245,1057,624],{"class":350},[245,1059,73],{"class":254},[245,1061,1062],{"class":392},"10",[245,1064,73],{"class":254},[245,1066,634],{"class":350},[245,1068,424],{"class":254},[245,1070,1071],{"class":164,"line":264},[245,1072,1073],{"class":254},"major.setFilterExpression(\n",[245,1075,1076,1079,1081,1083,1085],{"class":164,"line":277},[245,1077,1078],{"class":350},"    \"",[245,1080,651],{"class":392},[245,1082,72],{"class":350},[245,1084,651],{"class":392},[245,1086,1087],{"class":350}," IN ('motorway', 'trunk', 'primary') \"\n",[245,1089,1090,1093,1095,1098,1100,1103,1105,1107,1109],{"class":164,"line":283},[245,1091,1092],{"class":350},"    \"AND ",[245,1094,651],{"class":392},[245,1096,1097],{"class":350},"name",[245,1099,651],{"class":392},[245,1101,1102],{"class":350}," IS NOT NULL AND length(",[245,1104,651],{"class":392},[245,1106,1097],{"class":350},[245,1108,651],{"class":392},[245,1110,1111],{"class":350},") > 0\"\n",[245,1113,1114],{"class":164,"line":289},[245,1115,304],{"class":254},[245,1117,1118,1121,1124],{"class":164,"line":295},[245,1119,1120],{"class":254},"major.setMaximumScale(",[245,1122,1123],{"class":392},"500000",[245,1125,304],{"class":254},[245,1127,1128],{"class":164,"line":301},[245,1129,1130],{"class":254},"root.appendChild(major)\n",[14,1132,1133,1134,1138],{},"This same conditional thinking drives thematic styling; if you classify the underlying polygons too, pair this with ",[27,1135,1137],{"href":1136},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","Create a Choropleth Map in PyQGIS"," so labels and fill colors describe the same classes.",[52,1140,1142],{"id":1141},"setting-priority-per-rule","Setting Priority Per Rule",[14,1144,1145,1146,1149,1150,1153],{},"Scale ranges decide ",[36,1147,1148],{},"whether"," a rule is eligible at the current zoom, but when several eligible labels compete for the same space the placement engine drops some. Each rule's settings object carries its own ",[18,1151,1152],{},"priority",", so you control which classes survive crowding. Give the most important roads the highest priority.",[236,1155,1157],{"className":238,"code":1156,"language":240,"meta":241,"style":241},"# Inside road_label_settings, or applied after construction:\nmotorway_settings = road_label_settings(\"name\", 11, \"#7c2d12\")\nmotorway_settings.priority = 10        # never sacrificed\nsecondary_settings = road_label_settings(\"name\", 9, \"#1f2937\")\nsecondary_settings.priority = 6\nresidential_settings = road_label_settings(\"name\", 8, \"#374151\")\nresidential_settings.priority = 3      # first to go when crowded\n\nmotorway = QgsRuleBasedLabeling.Rule(motorway_settings)\nsecondary = QgsRuleBasedLabeling.Rule(secondary_settings)\nresidential = QgsRuleBasedLabeling.Rule(residential_settings)\n",[18,1158,1159,1164,1186,1199,1220,1230,1251,1264,1268,1277,1286],{"__ignoreMap":241},[245,1160,1161],{"class":164,"line":247},[245,1162,1163],{"class":582},"# Inside road_label_settings, or applied after construction:\n",[245,1165,1166,1169,1171,1174,1176,1178,1180,1182,1184],{"class":164,"line":264},[245,1167,1168],{"class":254},"motorway_settings ",[245,1170,338],{"class":250},[245,1172,1173],{"class":254}," road_label_settings(",[245,1175,624],{"class":350},[245,1177,73],{"class":254},[245,1179,629],{"class":392},[245,1181,73],{"class":254},[245,1183,634],{"class":350},[245,1185,304],{"class":254},[245,1187,1188,1191,1193,1196],{"class":164,"line":277},[245,1189,1190],{"class":254},"motorway_settings.priority ",[245,1192,338],{"class":250},[245,1194,1195],{"class":392}," 10",[245,1197,1198],{"class":582},"        # never sacrificed\n",[245,1200,1201,1204,1206,1208,1210,1212,1214,1216,1218],{"class":164,"line":283},[245,1202,1203],{"class":254},"secondary_settings ",[245,1205,338],{"class":250},[245,1207,1173],{"class":254},[245,1209,624],{"class":350},[245,1211,73],{"class":254},[245,1213,704],{"class":392},[245,1215,73],{"class":254},[245,1217,709],{"class":350},[245,1219,304],{"class":254},[245,1221,1222,1225,1227],{"class":164,"line":289},[245,1223,1224],{"class":254},"secondary_settings.priority ",[245,1226,338],{"class":250},[245,1228,1229],{"class":392}," 6\n",[245,1231,1232,1235,1237,1239,1241,1243,1245,1247,1249],{"class":164,"line":295},[245,1233,1234],{"class":254},"residential_settings ",[245,1236,338],{"class":250},[245,1238,1173],{"class":254},[245,1240,624],{"class":350},[245,1242,73],{"class":254},[245,1244,146],{"class":392},[245,1246,73],{"class":254},[245,1248,805],{"class":350},[245,1250,304],{"class":254},[245,1252,1253,1256,1258,1261],{"class":164,"line":301},[245,1254,1255],{"class":254},"residential_settings.priority ",[245,1257,338],{"class":250},[245,1259,1260],{"class":392}," 3",[245,1262,1263],{"class":582},"      # first to go when crowded\n",[245,1265,1266],{"class":164,"line":307},[245,1267,311],{"emptyLinePlaceholder":310},[245,1269,1270,1272,1274],{"class":164,"line":314},[245,1271,611],{"class":254},[245,1273,338],{"class":250},[245,1275,1276],{"class":254}," QgsRuleBasedLabeling.Rule(motorway_settings)\n",[245,1278,1279,1281,1283],{"class":164,"line":319},[245,1280,689],{"class":254},[245,1282,338],{"class":250},[245,1284,1285],{"class":254}," QgsRuleBasedLabeling.Rule(secondary_settings)\n",[245,1287,1288,1290,1292],{"class":164,"line":332},[245,1289,786],{"class":254},[245,1291,338],{"class":250},[245,1293,1294],{"class":254}," QgsRuleBasedLabeling.Rule(residential_settings)\n",[14,1296,1297,1299],{},[63,1298,523],{}," Priority runs 0 (lowest) to 10 (highest). When the engine cannot place every candidate, it discards low-priority labels first, so the motorway names at priority 10 persist while priority-3 residential streets vanish in dense areas. Priority and scale range work together: scale range removes a rule from contention entirely outside its zoom band, while priority arbitrates among the rules that remain. Tuning these two settings, rather than disabling collision detection, is how you keep a crowded street map legible.",[52,1301,1303],{"id":1302},"updating-a-single-rule-later","Updating a Single Rule Later",[14,1305,1306],{},"Because the rule tree is a mutable object, you can adjust one branch without rebuilding everything. Find a rule by its description (or iterate the tree) and change its filter, scale, or settings in place, then repaint.",[236,1308,1310],{"className":238,"code":1309,"language":240,"meta":241,"style":241},"labeling = layer.labeling()\nfor rule in labeling.rootRule().children():\n    if rule.description() == \"Residential streets\":\n        rule.setMaximumScale(15000)        # show even closer in only\nlayer.setLabeling(labeling.clone())\nlayer.triggerRepaint()\n",[18,1311,1312,1321,1333,1350,1364,1369],{"__ignoreMap":241},[245,1313,1314,1316,1318],{"class":164,"line":247},[245,1315,952],{"class":254},[245,1317,338],{"class":250},[245,1319,1320],{"class":254}," layer.labeling()\n",[245,1322,1323,1325,1328,1330],{"class":164,"line":264},[245,1324,990],{"class":250},[245,1326,1327],{"class":254}," rule ",[245,1329,996],{"class":250},[245,1331,1332],{"class":254}," labeling.rootRule().children():\n",[245,1334,1335,1338,1341,1344,1347],{"class":164,"line":277},[245,1336,1337],{"class":250},"    if",[245,1339,1340],{"class":254}," rule.description() ",[245,1342,1343],{"class":250},"==",[245,1345,1346],{"class":350}," \"Residential streets\"",[245,1348,1349],{"class":254},":\n",[245,1351,1352,1355,1358,1361],{"class":164,"line":283},[245,1353,1354],{"class":254},"        rule.setMaximumScale(",[245,1356,1357],{"class":392},"15000",[245,1359,1360],{"class":254},")        ",[245,1362,1363],{"class":582},"# show even closer in only\n",[245,1365,1366],{"class":164,"line":289},[245,1367,1368],{"class":254},"layer.setLabeling(labeling.clone())\n",[245,1370,1371],{"class":164,"line":295},[245,1372,976],{"class":254},[14,1374,1375,1377,1378,1381],{},[63,1376,523],{}," Mutating the rule changes the live tree, but reinstalling a ",[18,1379,1380],{},"clone()"," is the reliable way to force the layer to pick up the change across all QGIS versions in scope. This targeted-edit pattern keeps interactive tools responsive — a slider that controls when minor roads appear only needs to touch one rule, not regenerate the entire labeling.",[52,1383,1385],{"id":1384},"qgis-version-compatibility","QGIS Version Compatibility",[14,1387,1388,1389,1391,1392,1394],{},"All examples target ",[63,1390,65],{}," (Python 3.12) as the baseline. The ",[18,1393,20],{}," API is stable across the supported range.",[1396,1397,1398,1417],"table",{},[1399,1400,1401],"thead",{},[1402,1403,1404,1408,1411,1414],"tr",{},[1405,1406,1407],"th",{},"Component",[1405,1409,1410],{},"3.28 LTR",[1405,1412,1413],{},"3.34 LTR (baseline)",[1405,1415,1416],{},"3.40 \u002F 3.44",[1418,1419,1420,1434,1449,1464],"tbody",{},[1402,1421,1422,1427,1430,1432],{},[1423,1424,1425],"td",{},[18,1426,548],{},[1423,1428,1429],{},"available",[1423,1431,1429],{},[1423,1433,1429],{},[1402,1435,1436,1443,1445,1447],{},[1423,1437,1438,1440,1441],{},[18,1439,872],{}," \u002F ",[18,1442,556],{},[1423,1444,1429],{},[1423,1446,1429],{},[1423,1448,1429],{},[1402,1450,1451,1458,1460,1462],{},[1423,1452,1453,1440,1455],{},[18,1454,876],{},[18,1456,1457],{},"setMinimumScale",[1423,1459,1429],{},[1423,1461,1429],{},[1423,1463,1429],{},[1402,1465,1466,1473,1476,1478],{},[1423,1467,1468,1469,1472],{},"Placement enums (",[18,1470,1471],{},"QgsPalLayerSettings.Curved",")",[1423,1474,1475],{},"scoped names",[1423,1477,1475],{},[1423,1479,1480,1481],{},"scoped + ",[18,1482,1483],{},"Qgis.LabelPlacement",[14,1485,1486,1487,1489],{},"On QGIS 3.28 the only practical difference is Python 3.9 versus 3.12 — no labeling code changes are required. Avoid ",[18,1488,1483],{}," enums unless you have dropped 3.28 support.",[52,1491,1493],{"id":1492},"troubleshooting","Troubleshooting",[14,1495,1496,1499,1500,1503,1504,1507],{},[63,1497,1498],{},"No labels appear after running the script.","\nConfirm you called ",[18,1501,1502],{},"layer.setLabelsEnabled(True)"," after ",[18,1505,1506],{},"setLabeling()",". Then check that at least one rule's filter actually matches features (test the expression in the field calculator) and that the current map scale falls inside a rule's scale range.",[14,1509,1510,1513,1514,1516,1517,1520,1521,1523,1524,1526,1527,1530],{},[63,1511,1512],{},"Labels show at the wrong zoom levels.","\nThis is almost always the scale-method inversion. ",[18,1515,876],{}," controls the most-zoomed-",[36,1518,1519],{},"out"," limit (largest denominator) and ",[18,1522,1457],{}," the most-zoomed-",[36,1525,996],{}," limit. If a rule never shows when zoomed out, raise its ",[18,1528,1529],{},"maximumScale"," denominator.",[14,1532,1533,1539,1540,1542,1543,1546,1547,1550],{},[63,1534,1535,1538],{},[18,1536,1537],{},"TypeError"," when constructing the root rule.","\nPass ",[18,1541,552],{}," to ",[18,1544,1545],{},"QgsRuleBasedLabeling.Rule(None)"," for the container root, not an empty ",[18,1548,1549],{},"QgsPalLayerSettings()",". Child rules, by contrast, must receive a real settings object.",[14,1552,1553,1556,1557,1559],{},[63,1554,1555],{},"Some matching features stay unlabeled at busy scales.","\nThat is the collision engine dropping overlaps, not a rule failure. Raise ",[18,1558,1152],{}," on the important rule's settings or reduce font size; see the placement and obstacle discussion in the parent cluster.",[52,1561,1563],{"id":1562},"conclusion","Conclusion",[14,1565,1566,1567,1440,1570,1440,1573,1576],{},"Rule-based labeling turns a flat label layer into a scale-aware, class-aware map. Build a factory for your settings, assemble a root rule with filtered, scale-bounded children, then install it with the standard ",[18,1568,1569],{},"setLabeling",[18,1571,1572],{},"setLabelsEnabled",[18,1574,1575],{},"triggerRepaint"," trio. Because every rule speaks the QGIS expression language, the same technique extends to any attribute logic your data requires.",[52,1578,1580],{"id":1579},"frequently-asked-questions","Frequently Asked Questions",[14,1582,1583,1586,1587,1589],{},[63,1584,1585],{},"Can a rule have child rules of its own (nested deeper than one level)?","\nYes. ",[18,1588,556],{}," works at any depth, and filters compound — a child only applies to features that already passed its parent's filter. Use nesting to factor a shared filter (e.g. \"named roads\") into a parent and vary scale or style in the children.",[14,1591,1592,1595,1596,1599,1600,1602,1603,1605],{},[63,1593,1594],{},"How do I convert existing simple labeling into rule-based labeling?","\nRead the current settings with ",[18,1597,1598],{},"layer.labeling().settings()",", wrap them in a ",[18,1601,548],{},", append that to a new root, and reinstall. There is no automatic one-call converter, but reusing the existing ",[18,1604,24],{}," makes it a few lines.",[14,1607,1608,1611],{},[63,1609,1610],{},"Do rule scale ranges interact with the layer's own scale-dependent visibility?","\nThey are independent. The layer can be hidden entirely by its visibility scale range, while each label rule has its own range applied only when the layer is drawn. Both must permit the current scale for a label to show.",[52,1613,1615],{"id":1614},"related","Related",[57,1617,1618,1622,1627],{},[60,1619,1620],{},[27,1621,30],{"href":29},[60,1623,1624],{},[27,1625,1626],{"href":1032},"Set a Vector Layer Symbol Color in PyQGIS",[60,1628,1629],{},[27,1630,1137],{"href":1136},[1632,1633,1634],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":241,"searchDepth":264,"depth":264,"links":1636},[1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649],{"id":54,"depth":264,"text":55},{"id":94,"depth":264,"text":95},{"id":227,"depth":264,"text":228},{"id":542,"depth":264,"text":543},{"id":895,"depth":264,"text":896},{"id":1036,"depth":264,"text":1037},{"id":1141,"depth":264,"text":1142},{"id":1302,"depth":264,"text":1303},{"id":1384,"depth":264,"text":1385},{"id":1492,"depth":264,"text":1493},{"id":1562,"depth":264,"text":1563},{"id":1579,"depth":264,"text":1580},{"id":1614,"depth":264,"text":1615},"Build rule-based labeling in PyQGIS with QgsRuleBasedLabeling, nested rules, filter expressions, and scale ranges to label features differently by class.","md",{},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis",{"title":5,"description":1650},"pyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis\u002Findex","9KgJ0frTeH4WdBxJMPUBpDvy_mSN4RJZ6wKhfDzWfhg",1781781223069]