[{"data":1,"prerenderedAt":1520},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1509,"extension":1510,"meta":1511,"navigation":283,"path":1516,"seo":1517,"stem":1518,"__hash__":1519},"docs\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002Findex.md","Create a Rule-Based Renderer in PyQGIS",{"type":7,"value":8,"toc":1495},"minimark",[9,13,22,31,195,200,222,226,498,515,518,522,658,670,674,749,766,848,852,855,917,930,934,937,1022,1039,1043,1050,1173,1185,1192,1274,1278,1284,1351,1355,1403,1407,1413,1417,1423,1429,1439,1449,1459,1463,1491],[10,11,5],"h1",{"id":12},"create-a-rule-based-renderer-in-pyqgis",[14,15,16,17,21],"p",{},"Categorised and graduated renderers each answer one question: what value is in this field, and which band does this number fall into. Real cartography asks compound questions. Draw motorways at every scale but minor roads only below 1:50 000; colour by surface type, but only for roads that are actually open; and render everything left over in grey so nothing silently disappears. That is a rule-based renderer, and in Python it is a small tree of ",[18,19,20],"code",{},"QgsRuleBasedRenderer.Rule"," objects.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","Graduated and Categorised Renderers in PyQGIS",". It covers building rules, nesting them, scale-dependent rendering, the else rule, and converting an existing renderer into rules you can extend.",[14,32,33],{},[34,35,40,44,48,55,72,81,89,94,100,109,115,119,125,128,131,136,140,143,150,155,159,162,165,168,175,177,181,186,190],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 284","img","A rule tree with a root rule, two branches filtered by road class each carrying a scale range, and an else rule catching everything unmatched","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"The shape of a rule tree",[45,46,47],"desc",{},"A root rule holds three children. The first matches motorways and has no scale limit. The second matches minor roads and only draws below one to fifty thousand. The third is an else rule with no filter, which catches every feature the earlier rules did not match and draws it in grey. Nested under the first rule are two further rules splitting motorways by surface.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","284","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"ruleArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#59645f",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Every feature is tested against every top-level rule",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":78,"style":88},"300","48","160","42","#fffdf7","stroke-width:2.5",[73,90,93],{"x":75,"y":91,"style":92,"fill":78,"textAnchor":79},"68","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","root rule",[73,95,99],{"x":75,"y":96,"style":97,"fill":98,"textAnchor":79},"84","text-anchor:middle;font-size:10px;font-family:sans-serif","#2f3b35","no filter, no symbol",[49,101],{"x":102,"y":103,"width":104,"height":105,"rx":63,"fill":106,"stroke":107,"style":108},"20","120","212","60","#eff3ff","#2563eb","stroke-width:2",[73,110,114],{"x":111,"y":112,"style":113,"fill":107,"textAnchor":79},"126","142","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","\"class\" = 'motorway'",[73,116,118],{"x":111,"y":117,"style":97,"fill":98,"textAnchor":79},"162","all scales · 2.0 mm blue",[49,120],{"x":121,"y":103,"width":122,"height":105,"rx":63,"fill":123,"stroke":124,"style":108},"272","216","#edf8e9","#15803d",[73,126,127],{"x":75,"y":112,"style":113,"fill":124,"textAnchor":79},"\"class\" = 'minor'",[73,129,130],{"x":75,"y":117,"style":97,"fill":98,"textAnchor":79},"below 1:50 000 only",[49,132],{"x":133,"y":103,"width":122,"height":105,"rx":63,"fill":134,"stroke":135,"style":108},"528","#fdf2e2","#b45309",[73,137,139],{"x":138,"y":112,"style":113,"fill":135,"textAnchor":79},"636","ELSE",[73,141,142],{"x":138,"y":117,"style":97,"fill":98,"textAnchor":79},"everything unmatched, grey",[49,144],{"x":102,"y":145,"width":146,"height":147,"rx":148,"fill":87,"stroke":71,"style":149},"204","100","56","6","stroke-width:1.5",[73,151,154],{"x":152,"y":153,"style":97,"fill":98,"textAnchor":79},"70","226","surface =",[73,156,158],{"x":152,"y":157,"style":97,"fill":98,"textAnchor":79},"242","'asphalt'",[49,160],{"x":161,"y":145,"width":146,"height":147,"rx":148,"fill":87,"stroke":71,"style":149},"132",[73,163,154],{"x":164,"y":153,"style":97,"fill":98,"textAnchor":79},"182",[73,166,167],{"x":164,"y":157,"style":97,"fill":98,"textAnchor":79},"'concrete'",[169,170],"line",{"x1":171,"y1":172,"x2":85,"y2":173,"stroke":71,"style":174},"330","90","114","stroke-width:2;marker-end:url(#ruleArrow)",[169,176],{"x1":75,"y1":172,"x2":75,"y2":173,"stroke":71,"style":174},[169,178],{"x1":179,"y1":172,"x2":180,"y2":173,"stroke":71,"style":174},"430","600",[169,182],{"x1":146,"y1":183,"x2":184,"y2":185,"stroke":71,"style":174},"180","80","198",[169,187],{"x1":188,"y1":183,"x2":189,"y2":185,"stroke":71,"style":174},"152","172",[73,191,194],{"x":75,"y":192,"style":193,"fill":71,"textAnchor":79},"276","text-anchor:middle;font-size:11px;font-family:sans-serif","A child rule is tested only if its parent matched — nesting is an AND",[196,197,199],"h2",{"id":198},"prerequisites","Prerequisites",[201,202,203,211,214],"ul",{},[204,205,206,210],"li",{},[207,208,209],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[204,212,213],{},"A vector layer with attributes worth branching on.",[204,215,216,217,221],{},"Familiarity with QGIS expression syntax — see ",[26,218,220],{"href":219},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","Evaluate a QGIS Expression in PyQGIS",".",[196,223,225],{"id":224},"build-a-rule-tree","Build a rule tree",[227,228,233],"pre",{"className":229,"code":230,"language":231,"meta":232,"style":232},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsRuleBasedRenderer, QgsSymbol, QgsProject, QgsWkbTypes,\n)\nfrom qgis.PyQt.QtGui import QColor\n\nlayer = QgsProject.instance().mapLayersByName(\"roads\")[0]\n\nroot = QgsRuleBasedRenderer.Rule(None)\n\ndef make_rule(label, expression, colour, width):\n    symbol = QgsSymbol.defaultSymbol(layer.geometryType())\n    symbol.setColor(QColor(colour))\n    symbol.setWidth(width)\n    rule = QgsRuleBasedRenderer.Rule(symbol)\n    rule.setLabel(label)\n    rule.setFilterExpression(expression)\n    return rule\n\nroot.appendChild(make_rule(\"Motorway\", \"\\\"class\\\" = 'motorway'\", \"#2563eb\", 1.2))\nroot.appendChild(make_rule(\"A road\", \"\\\"class\\\" = 'primary'\", \"#0f766e\", 0.8))\n\nlayer.setRenderer(QgsRuleBasedRenderer(root))\nlayer.triggerRepaint()\n","python","",[18,234,235,253,259,265,278,285,310,315,331,336,349,360,366,372,383,389,395,404,409,448,481,486,492],{"__ignoreMap":232},[236,237,239,243,247,250],"span",{"class":169,"line":238},1,[236,240,242],{"class":241},"snl16","from",[236,244,246],{"class":245},"s95oV"," qgis.core ",[236,248,249],{"class":241},"import",[236,251,252],{"class":245}," (\n",[236,254,256],{"class":169,"line":255},2,[236,257,258],{"class":245},"    QgsRuleBasedRenderer, QgsSymbol, QgsProject, QgsWkbTypes,\n",[236,260,262],{"class":169,"line":261},3,[236,263,264],{"class":245},")\n",[236,266,268,270,273,275],{"class":169,"line":267},4,[236,269,242],{"class":241},[236,271,272],{"class":245}," qgis.PyQt.QtGui ",[236,274,249],{"class":241},[236,276,277],{"class":245}," QColor\n",[236,279,281],{"class":169,"line":280},5,[236,282,284],{"emptyLinePlaceholder":283},true,"\n",[236,286,288,291,294,297,301,304,307],{"class":169,"line":287},6,[236,289,290],{"class":245},"layer ",[236,292,293],{"class":241},"=",[236,295,296],{"class":245}," QgsProject.instance().mapLayersByName(",[236,298,300],{"class":299},"sU2Wk","\"roads\"",[236,302,303],{"class":245},")[",[236,305,51],{"class":306},"sDLfK",[236,308,309],{"class":245},"]\n",[236,311,313],{"class":169,"line":312},7,[236,314,284],{"emptyLinePlaceholder":283},[236,316,318,321,323,326,329],{"class":169,"line":317},8,[236,319,320],{"class":245},"root ",[236,322,293],{"class":241},[236,324,325],{"class":245}," QgsRuleBasedRenderer.Rule(",[236,327,328],{"class":306},"None",[236,330,264],{"class":245},[236,332,334],{"class":169,"line":333},9,[236,335,284],{"emptyLinePlaceholder":283},[236,337,339,342,346],{"class":169,"line":338},10,[236,340,341],{"class":241},"def",[236,343,345],{"class":344},"svObZ"," make_rule",[236,347,348],{"class":245},"(label, expression, colour, width):\n",[236,350,352,355,357],{"class":169,"line":351},11,[236,353,354],{"class":245},"    symbol ",[236,356,293],{"class":241},[236,358,359],{"class":245}," QgsSymbol.defaultSymbol(layer.geometryType())\n",[236,361,363],{"class":169,"line":362},12,[236,364,365],{"class":245},"    symbol.setColor(QColor(colour))\n",[236,367,369],{"class":169,"line":368},13,[236,370,371],{"class":245},"    symbol.setWidth(width)\n",[236,373,375,378,380],{"class":169,"line":374},14,[236,376,377],{"class":245},"    rule ",[236,379,293],{"class":241},[236,381,382],{"class":245}," QgsRuleBasedRenderer.Rule(symbol)\n",[236,384,386],{"class":169,"line":385},15,[236,387,388],{"class":245},"    rule.setLabel(label)\n",[236,390,392],{"class":169,"line":391},16,[236,393,394],{"class":245},"    rule.setFilterExpression(expression)\n",[236,396,398,401],{"class":169,"line":397},17,[236,399,400],{"class":241},"    return",[236,402,403],{"class":245}," rule\n",[236,405,407],{"class":169,"line":406},18,[236,408,284],{"emptyLinePlaceholder":283},[236,410,412,415,418,421,424,427,430,432,435,437,440,442,445],{"class":169,"line":411},19,[236,413,414],{"class":245},"root.appendChild(make_rule(",[236,416,417],{"class":299},"\"Motorway\"",[236,419,420],{"class":245},", ",[236,422,423],{"class":299},"\"",[236,425,426],{"class":306},"\\\"",[236,428,429],{"class":299},"class",[236,431,426],{"class":306},[236,433,434],{"class":299}," = 'motorway'\"",[236,436,420],{"class":245},[236,438,439],{"class":299},"\"#2563eb\"",[236,441,420],{"class":245},[236,443,444],{"class":306},"1.2",[236,446,447],{"class":245},"))\n",[236,449,451,453,456,458,460,462,464,466,469,471,474,476,479],{"class":169,"line":450},20,[236,452,414],{"class":245},[236,454,455],{"class":299},"\"A road\"",[236,457,420],{"class":245},[236,459,423],{"class":299},[236,461,426],{"class":306},[236,463,429],{"class":299},[236,465,426],{"class":306},[236,467,468],{"class":299}," = 'primary'\"",[236,470,420],{"class":245},[236,472,473],{"class":299},"\"#0f766e\"",[236,475,420],{"class":245},[236,477,478],{"class":306},"0.8",[236,480,447],{"class":245},[236,482,484],{"class":169,"line":483},21,[236,485,284],{"emptyLinePlaceholder":283},[236,487,489],{"class":169,"line":488},22,[236,490,491],{"class":245},"layer.setRenderer(QgsRuleBasedRenderer(root))\n",[236,493,495],{"class":169,"line":494},23,[236,496,497],{"class":245},"layer.triggerRepaint()\n",[14,499,500,503,504,506,507,510,511,514],{},[207,501,502],{},"Breakdown:"," The root rule is constructed with ",[18,505,328],{}," as its symbol — it draws nothing itself and exists only to hold children. Each child owns its own symbol, and ",[18,508,509],{},"QgsSymbol.defaultSymbol(layer.geometryType())"," produces the right kind (marker, line or fill) without a type check. ",[18,512,513],{},"setLabel()"," is what appears in the legend, so leaving it empty produces a legend entry with no text. Field names inside the expression are double-quoted and literals single-quoted, which is why the Python string needs escaping — a raw string or single-quoted Python string avoids the backslashes.",[14,516,517],{},"Rules are evaluated independently: a feature matching two top-level rules is drawn twice, once per rule. That is a feature, not a bug — it is how a casing-and-fill road style is built — but it surprises people expecting first-match-wins behaviour.",[196,519,521],{"id":520},"nest-rules-to-combine-conditions","Nest rules to combine conditions",[227,523,525],{"className":229,"code":524,"language":231,"meta":232,"style":232},"motorway = make_rule(\"Motorway\", \"\\\"class\\\" = 'motorway'\", \"#2563eb\", 1.2)\n\nlit = make_rule(\"Lit\", \"\\\"lit\\\" = 'yes'\", \"#f59e0b\", 1.4)\nunlit = make_rule(\"Unlit\", \"\\\"lit\\\" \u003C> 'yes'\", \"#1e40af\", 1.2)\n\nmotorway.appendChild(lit)\nmotorway.appendChild(unlit)\nroot.appendChild(motorway)\n",[18,526,527,561,565,603,639,643,648,653],{"__ignoreMap":232},[236,528,529,532,534,537,539,541,543,545,547,549,551,553,555,557,559],{"class":169,"line":238},[236,530,531],{"class":245},"motorway ",[236,533,293],{"class":241},[236,535,536],{"class":245}," make_rule(",[236,538,417],{"class":299},[236,540,420],{"class":245},[236,542,423],{"class":299},[236,544,426],{"class":306},[236,546,429],{"class":299},[236,548,426],{"class":306},[236,550,434],{"class":299},[236,552,420],{"class":245},[236,554,439],{"class":299},[236,556,420],{"class":245},[236,558,444],{"class":306},[236,560,264],{"class":245},[236,562,563],{"class":169,"line":255},[236,564,284],{"emptyLinePlaceholder":283},[236,566,567,570,572,574,577,579,581,583,586,588,591,593,596,598,601],{"class":169,"line":261},[236,568,569],{"class":245},"lit ",[236,571,293],{"class":241},[236,573,536],{"class":245},[236,575,576],{"class":299},"\"Lit\"",[236,578,420],{"class":245},[236,580,423],{"class":299},[236,582,426],{"class":306},[236,584,585],{"class":299},"lit",[236,587,426],{"class":306},[236,589,590],{"class":299}," = 'yes'\"",[236,592,420],{"class":245},[236,594,595],{"class":299},"\"#f59e0b\"",[236,597,420],{"class":245},[236,599,600],{"class":306},"1.4",[236,602,264],{"class":245},[236,604,605,608,610,612,615,617,619,621,623,625,628,630,633,635,637],{"class":169,"line":267},[236,606,607],{"class":245},"unlit ",[236,609,293],{"class":241},[236,611,536],{"class":245},[236,613,614],{"class":299},"\"Unlit\"",[236,616,420],{"class":245},[236,618,423],{"class":299},[236,620,426],{"class":306},[236,622,585],{"class":299},[236,624,426],{"class":306},[236,626,627],{"class":299}," \u003C> 'yes'\"",[236,629,420],{"class":245},[236,631,632],{"class":299},"\"#1e40af\"",[236,634,420],{"class":245},[236,636,444],{"class":306},[236,638,264],{"class":245},[236,640,641],{"class":169,"line":280},[236,642,284],{"emptyLinePlaceholder":283},[236,644,645],{"class":169,"line":287},[236,646,647],{"class":245},"motorway.appendChild(lit)\n",[236,649,650],{"class":169,"line":312},[236,651,652],{"class":245},"motorway.appendChild(unlit)\n",[236,654,655],{"class":169,"line":317},[236,656,657],{"class":245},"root.appendChild(motorway)\n",[14,659,660,662,663,666,667,669],{},[207,661,502],{}," A child rule is only evaluated for features that matched its parent, so nesting expresses ",[18,664,665],{},"class = 'motorway' AND lit = 'yes'"," without repeating the first condition. The parent still draws its own symbol unless it was created with ",[18,668,328],{},"; to use the parent purely as a grouping condition, construct it symbol-less and let the children do the drawing. Nesting also keeps the legend tidy, because QGIS renders the tree structure in the layer panel.",[196,671,673],{"id":672},"make-rules-scale-dependent","Make rules scale-dependent",[227,675,677],{"className":229,"code":676,"language":231,"meta":232,"style":232},"minor = make_rule(\"Minor road\", \"\\\"class\\\" = 'minor'\", \"#59645f\", 0.4)\nminor.setScaleMinDenom(0)          # no lower limit — visible when zoomed in\nminor.setScaleMaxDenom(50000)      # hidden above 1:50 000\nroot.appendChild(minor)\n",[18,678,679,716,730,744],{"__ignoreMap":232},[236,680,681,684,686,688,691,693,695,697,699,701,704,706,709,711,714],{"class":169,"line":238},[236,682,683],{"class":245},"minor ",[236,685,293],{"class":241},[236,687,536],{"class":245},[236,689,690],{"class":299},"\"Minor road\"",[236,692,420],{"class":245},[236,694,423],{"class":299},[236,696,426],{"class":306},[236,698,429],{"class":299},[236,700,426],{"class":306},[236,702,703],{"class":299}," = 'minor'\"",[236,705,420],{"class":245},[236,707,708],{"class":299},"\"#59645f\"",[236,710,420],{"class":245},[236,712,713],{"class":306},"0.4",[236,715,264],{"class":245},[236,717,718,721,723,726],{"class":169,"line":255},[236,719,720],{"class":245},"minor.setScaleMinDenom(",[236,722,51],{"class":306},[236,724,725],{"class":245},")          ",[236,727,729],{"class":728},"sjoCn","# no lower limit — visible when zoomed in\n",[236,731,732,735,738,741],{"class":169,"line":261},[236,733,734],{"class":245},"minor.setScaleMaxDenom(",[236,736,737],{"class":306},"50000",[236,739,740],{"class":245},")      ",[236,742,743],{"class":728},"# hidden above 1:50 000\n",[236,745,746],{"class":169,"line":267},[236,747,748],{"class":245},"root.appendChild(minor)\n",[14,750,751,753,754,758,759,762,763,765],{},[207,752,502],{}," The denominators are the scale ",[755,756,757],"em",{},"numbers",": ",[18,760,761],{},"setScaleMaxDenom(50000)"," means the rule stops drawing when the map is zoomed out past 1:50 000, because larger denominators are smaller scales. Setting the minimum to ",[18,764,51],{}," means no lower bound. This is the mechanism behind every legible multi-scale map — it lets a dense network thin out as the reader zooms out, rather than collapsing into a solid block of ink — and it is per rule, so different classes can disappear at different scales.",[14,767,768],{},[34,769,772,775,778,781,784,790,795,811,814,817,821,829,832,836,840,845],{"viewBox":770,"role":37,"ariaLabel":771,"xmlns":39},"0 0 760 260","The same road network drawn at three scales, showing minor roads present when zoomed in, dropping out at medium scale and only motorways remaining when zoomed out",[41,773,774],{},"Scale ranges thin the network as the reader zooms out",[45,776,777],{},"Three panels show the same area at increasing scale denominators. Zoomed in, motorways, A roads and minor roads all draw. At one to fifty thousand the minor roads drop out and the map stays readable. Zoomed further out only motorways remain, so the shape of the network is still visible rather than a solid mass of lines.",[49,779],{"x":51,"y":51,"width":52,"height":780,"fill":54},"260",[73,782,783],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Each rule chooses the scales at which it is worth drawing",[49,785],{"x":786,"y":84,"width":787,"height":788,"rx":789,"fill":87,"stroke":71,"style":108},"16","236","176","10",[73,791,794],{"x":792,"y":793,"style":92,"fill":78,"textAnchor":79},"134","72","1:10 000",[796,797,799,803,807],"g",{"fill":798},"none",[68,800],{"d":801,"stroke":107,"style":802},"M36 190 L232 96","stroke-width:4",[68,804],{"d":805,"stroke":806,"style":88},"M36 130 L232 160","#0f766e",[68,808],{"d":809,"stroke":71,"style":810},"M60 200 L92 100 M120 200 L140 100 M170 200 L184 104 M40 160 L228 132 M44 176 L224 190","stroke-width:1",[73,812,813],{"x":792,"y":104,"style":193,"fill":98,"textAnchor":79},"every class drawn",[49,815],{"x":816,"y":84,"width":787,"height":788,"rx":789,"fill":87,"stroke":71,"style":108},"264",[73,818,820],{"x":819,"y":793,"style":92,"fill":78,"textAnchor":79},"382","1:50 000",[796,822,823,826],{"fill":798},[68,824],{"d":825,"stroke":107,"style":802},"M284 190 L480 96",[68,827],{"d":828,"stroke":806,"style":88},"M284 130 L480 160",[73,830,831],{"x":819,"y":104,"style":193,"fill":124,"textAnchor":79},"minor roads have dropped out",[49,833],{"x":834,"y":84,"width":835,"height":788,"rx":789,"fill":87,"stroke":71,"style":108},"512","232",[73,837,839],{"x":838,"y":793,"style":92,"fill":78,"textAnchor":79},"628","1:250 000",[796,841,842],{"fill":798},[68,843],{"d":844,"stroke":107,"style":802},"M532 190 L728 96",[73,846,847],{"x":838,"y":104,"style":193,"fill":124,"textAnchor":79},"the network shape survives",[196,849,851],{"id":850},"always-add-an-else-rule","Always add an else rule",[14,853,854],{},"A feature matching no rule is not drawn. On a layer whose attribute values you do not fully control, that means data disappearing silently.",[227,856,858],{"className":229,"code":857,"language":231,"meta":232,"style":232},"fallback = QgsSymbol.defaultSymbol(layer.geometryType())\nfallback.setColor(QColor(\"#59645f\"))\n\nelse_rule = QgsRuleBasedRenderer.Rule(fallback)\nelse_rule.setLabel(\"Other\")\nelse_rule.setIsElse(True)\nroot.appendChild(else_rule)\n",[18,859,860,869,878,882,892,902,912],{"__ignoreMap":232},[236,861,862,865,867],{"class":169,"line":238},[236,863,864],{"class":245},"fallback ",[236,866,293],{"class":241},[236,868,359],{"class":245},[236,870,871,874,876],{"class":169,"line":255},[236,872,873],{"class":245},"fallback.setColor(QColor(",[236,875,708],{"class":299},[236,877,447],{"class":245},[236,879,880],{"class":169,"line":261},[236,881,284],{"emptyLinePlaceholder":283},[236,883,884,887,889],{"class":169,"line":267},[236,885,886],{"class":245},"else_rule ",[236,888,293],{"class":241},[236,890,891],{"class":245}," QgsRuleBasedRenderer.Rule(fallback)\n",[236,893,894,897,900],{"class":169,"line":280},[236,895,896],{"class":245},"else_rule.setLabel(",[236,898,899],{"class":299},"\"Other\"",[236,901,264],{"class":245},[236,903,904,907,910],{"class":169,"line":287},[236,905,906],{"class":245},"else_rule.setIsElse(",[236,908,909],{"class":306},"True",[236,911,264],{"class":245},[236,913,914],{"class":169,"line":312},[236,915,916],{"class":245},"root.appendChild(else_rule)\n",[14,918,919,921,922,925,926,221],{},[207,920,502],{}," ",[18,923,924],{},"setIsElse(True)"," marks the rule as the catch-all: it matches every feature that no sibling rule matched, and it must be the last child for that to be meaningful. Adding one converts an invisible data problem into a visible grey line — a new road class appearing in next quarter's extract shows up immediately rather than being quietly omitted from every map. This is the same defensive instinct as the plausibility checks in ",[26,927,929],{"href":928},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts\u002F","Handle Errors and Logging in Unattended Scripts",[196,931,933],{"id":932},"convert-an-existing-renderer-to-rules","Convert an existing renderer to rules",[14,935,936],{},"Rather than rebuilding a categorised renderer by hand, convert it and then extend it.",[227,938,940],{"className":229,"code":939,"language":231,"meta":232,"style":232},"from qgis.core import QgsRuleBasedRenderer\n\nexisting = layer.renderer()                       # categorised or graduated\nconverted = QgsRuleBasedRenderer.convertFromRenderer(existing)\nlayer.setRenderer(converted)\n\nroot = layer.renderer().rootRule()\nfor rule in root.children():\n    rule.setScaleMaxDenom(100000)\n",[18,941,942,953,957,970,980,985,989,998,1012],{"__ignoreMap":232},[236,943,944,946,948,950],{"class":169,"line":238},[236,945,242],{"class":241},[236,947,246],{"class":245},[236,949,249],{"class":241},[236,951,952],{"class":245}," QgsRuleBasedRenderer\n",[236,954,955],{"class":169,"line":255},[236,956,284],{"emptyLinePlaceholder":283},[236,958,959,962,964,967],{"class":169,"line":261},[236,960,961],{"class":245},"existing ",[236,963,293],{"class":241},[236,965,966],{"class":245}," layer.renderer()                       ",[236,968,969],{"class":728},"# categorised or graduated\n",[236,971,972,975,977],{"class":169,"line":267},[236,973,974],{"class":245},"converted ",[236,976,293],{"class":241},[236,978,979],{"class":245}," QgsRuleBasedRenderer.convertFromRenderer(existing)\n",[236,981,982],{"class":169,"line":280},[236,983,984],{"class":245},"layer.setRenderer(converted)\n",[236,986,987],{"class":169,"line":287},[236,988,284],{"emptyLinePlaceholder":283},[236,990,991,993,995],{"class":169,"line":312},[236,992,320],{"class":245},[236,994,293],{"class":241},[236,996,997],{"class":245}," layer.renderer().rootRule()\n",[236,999,1000,1003,1006,1009],{"class":169,"line":317},[236,1001,1002],{"class":241},"for",[236,1004,1005],{"class":245}," rule ",[236,1007,1008],{"class":241},"in",[236,1010,1011],{"class":245}," root.children():\n",[236,1013,1014,1017,1020],{"class":169,"line":333},[236,1015,1016],{"class":245},"    rule.setScaleMaxDenom(",[236,1018,1019],{"class":306},"100000",[236,1021,264],{"class":245},[14,1023,1024,921,1026,1029,1030,1034,1035,1038],{},[207,1025,502],{},[18,1027,1028],{},"convertFromRenderer()"," accepts a categorised, graduated or single-symbol renderer and returns an equivalent rule tree — one rule per category or class, filters already written. That makes it the fastest way to start: build the classification with the ",[26,1031,1033],{"href":1032},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis\u002F","natural breaks helper",", convert, then add scale ranges or an else rule. Iterating ",[18,1036,1037],{},"root.children()"," gives the generated rules, which can be modified in place before the layer repaints.",[196,1040,1042],{"id":1041},"drive-symbol-properties-from-expressions","Drive symbol properties from expressions",[14,1044,1045,1046,1049],{},"A rule chooses ",[755,1047,1048],{},"which"," symbol draws a feature. A data-defined property lets that one symbol vary per feature, which often replaces a dozen rules with one.",[227,1051,1053],{"className":229,"code":1052,"language":231,"meta":232,"style":232},"from qgis.core import QgsProperty, QgsSymbolLayer\n\nsymbol = QgsSymbol.defaultSymbol(layer.geometryType())\nsymbol_layer = symbol.symbolLayer(0)\n\nsymbol_layer.setDataDefinedProperty(\n    QgsSymbolLayer.PropertyStrokeWidth,\n    QgsProperty.fromExpression('scale_linear(\"traffic\", 0, 20000, 0.3, 3.0)'),\n)\nsymbol_layer.setDataDefinedProperty(\n    QgsSymbolLayer.PropertyStrokeColor,\n    QgsProperty.fromExpression(\n        \"\"\"CASE WHEN \"condition\" = 'poor' THEN '#b91c1c' ELSE '#0f766e' END\"\"\"\n    ),\n)\n\nrule = QgsRuleBasedRenderer.Rule(symbol)\nrule.setLabel(\"Roads by traffic and condition\")\n",[18,1054,1055,1066,1070,1079,1093,1097,1102,1107,1118,1122,1126,1131,1136,1141,1146,1150,1154,1163],{"__ignoreMap":232},[236,1056,1057,1059,1061,1063],{"class":169,"line":238},[236,1058,242],{"class":241},[236,1060,246],{"class":245},[236,1062,249],{"class":241},[236,1064,1065],{"class":245}," QgsProperty, QgsSymbolLayer\n",[236,1067,1068],{"class":169,"line":255},[236,1069,284],{"emptyLinePlaceholder":283},[236,1071,1072,1075,1077],{"class":169,"line":261},[236,1073,1074],{"class":245},"symbol ",[236,1076,293],{"class":241},[236,1078,359],{"class":245},[236,1080,1081,1084,1086,1089,1091],{"class":169,"line":267},[236,1082,1083],{"class":245},"symbol_layer ",[236,1085,293],{"class":241},[236,1087,1088],{"class":245}," symbol.symbolLayer(",[236,1090,51],{"class":306},[236,1092,264],{"class":245},[236,1094,1095],{"class":169,"line":280},[236,1096,284],{"emptyLinePlaceholder":283},[236,1098,1099],{"class":169,"line":287},[236,1100,1101],{"class":245},"symbol_layer.setDataDefinedProperty(\n",[236,1103,1104],{"class":169,"line":312},[236,1105,1106],{"class":245},"    QgsSymbolLayer.PropertyStrokeWidth,\n",[236,1108,1109,1112,1115],{"class":169,"line":317},[236,1110,1111],{"class":245},"    QgsProperty.fromExpression(",[236,1113,1114],{"class":299},"'scale_linear(\"traffic\", 0, 20000, 0.3, 3.0)'",[236,1116,1117],{"class":245},"),\n",[236,1119,1120],{"class":169,"line":333},[236,1121,264],{"class":245},[236,1123,1124],{"class":169,"line":338},[236,1125,1101],{"class":245},[236,1127,1128],{"class":169,"line":351},[236,1129,1130],{"class":245},"    QgsSymbolLayer.PropertyStrokeColor,\n",[236,1132,1133],{"class":169,"line":362},[236,1134,1135],{"class":245},"    QgsProperty.fromExpression(\n",[236,1137,1138],{"class":169,"line":368},[236,1139,1140],{"class":299},"        \"\"\"CASE WHEN \"condition\" = 'poor' THEN '#b91c1c' ELSE '#0f766e' END\"\"\"\n",[236,1142,1143],{"class":169,"line":374},[236,1144,1145],{"class":245},"    ),\n",[236,1147,1148],{"class":169,"line":385},[236,1149,264],{"class":245},[236,1151,1152],{"class":169,"line":391},[236,1153,284],{"emptyLinePlaceholder":283},[236,1155,1156,1159,1161],{"class":169,"line":397},[236,1157,1158],{"class":245},"rule ",[236,1160,293],{"class":241},[236,1162,382],{"class":245},[236,1164,1165,1168,1171],{"class":169,"line":406},[236,1166,1167],{"class":245},"rule.setLabel(",[236,1169,1170],{"class":299},"\"Roads by traffic and condition\"",[236,1172,264],{"class":245},[14,1174,1175,921,1177,1180,1181,1184],{},[207,1176,502],{},[18,1178,1179],{},"scale_linear()"," maps a data range onto a width range, clamping at both ends, so a road carrying twenty thousand vehicles draws at 3 mm and an empty lane at 0.3 mm with everything in between interpolated — a continuous encoding no set of discrete rules can match. The colour expression returns a hex string, which is how a ",[18,1182,1183],{},"CASE"," becomes symbology. Both are evaluated per feature at render time, so a change in the underlying data is reflected without touching the renderer.",[14,1186,1187,1188,221],{},"The trade-off is legibility in the legend: a data-defined width has no natural legend entry, because there are no classes to list. The usual answer is a hybrid — rules for the categories a reader must be able to look up, data-defined properties for the continuous variation within them. The expression syntax is the same one covered in ",[26,1189,1191],{"href":1190},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis\u002F","Data-Defined Symbol Size Expression in PyQGIS",[14,1193,1194],{},[34,1195,1198,1201,1204,1207,1210,1214,1219,1239,1242,1246,1271],{"viewBox":1196,"role":37,"ariaLabel":1197,"xmlns":39},"0 0 760 246","Discrete rule classes producing five stepped line widths compared with a data-defined expression producing a continuous width ramp across the same data range",[41,1199,1200],{},"Stepped classes against a continuous encoding",[45,1202,1203],{},"Five rules produce five stepped line widths, so every road within a class draws identically and the boundaries between classes are visible as jumps. A data-defined stroke width driven by scale linear produces a smooth ramp across the same range, encoding the actual value of every feature.",[49,1205],{"x":51,"y":51,"width":52,"height":1206,"fill":54},"246",[73,1208,1209],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Five classes, or every value",[49,1211],{"x":786,"y":84,"width":1212,"height":1213,"rx":789,"fill":87,"stroke":71,"style":108},"356","164",[73,1215,1218],{"x":1216,"y":1217,"style":92,"fill":78,"textAnchor":79},"194","74","five rules, five widths",[796,1220,1221,1224,1227,1231,1235],{"stroke":806,"fill":798},[68,1222],{"d":1223,"style":810},"M48 100 H340",[68,1225],{"d":1226,"style":108},"M48 124 H340",[68,1228],{"d":1229,"style":1230},"M48 148 H340","stroke-width:3.5",[68,1232],{"d":1233,"style":1234},"M48 172 H340","stroke-width:5",[68,1236],{"d":1237,"style":1238},"M48 196 H340","stroke-width:7",[49,1240],{"x":1241,"y":84,"width":1212,"height":1213,"rx":789,"fill":123,"stroke":124,"style":88},"388",[73,1243,1245],{"x":1244,"y":1217,"style":92,"fill":124,"textAnchor":79},"566","one rule, scale_linear()",[796,1247,1248,1251,1255,1259,1263,1267],{"stroke":806,"fill":798},[68,1249],{"d":1250,"style":810},"M420 100 H712",[68,1252],{"d":1253,"style":1254},"M420 116 H712","stroke-width:1.8",[68,1256],{"d":1257,"style":1258},"M420 133 H712","stroke-width:2.6",[68,1260],{"d":1261,"style":1262},"M420 151 H712","stroke-width:3.4",[68,1264],{"d":1265,"style":1266},"M420 170 H712","stroke-width:4.6",[68,1268],{"d":1269,"style":1270},"M420 191 H712","stroke-width:6",[73,1272,1273],{"x":75,"y":787,"style":193,"fill":71,"textAnchor":79},"Classes give a legend a reader can look up; a ramp gives every feature its own value",[196,1275,1277],{"id":1276},"qgis-version-compatibility","QGIS version compatibility",[14,1279,1280,1281,1283],{},"The examples target ",[207,1282,209],{}," (Python 3.12).",[1285,1286,1287,1303],"table",{},[1288,1289,1290],"thead",{},[1291,1292,1293,1297,1300],"tr",{},[1294,1295,1296],"th",{},"QGIS version",[1294,1298,1299],{},"Python",[1294,1301,1302],{},"Notes",[1304,1305,1306,1320,1330,1341],"tbody",{},[1291,1307,1308,1312,1315],{},[1309,1310,1311],"td",{},"3.22 LTR",[1309,1313,1314],{},"3.9",[1309,1316,1317,1318,221],{},"Full rule API including ",[18,1319,1028],{},[1291,1321,1322,1325,1327],{},[1309,1323,1324],{},"3.28 LTR",[1309,1326,1314],{},[1309,1328,1329],{},"Behaviour matches this page.",[1291,1331,1332,1335,1338],{},[1309,1333,1334],{},"3.34 LTR",[1309,1336,1337],{},"3.12",[1309,1339,1340],{},"Baseline for this page.",[1291,1342,1343,1346,1348],{},[1309,1344,1345],{},"3.40 \u002F 3.44",[1309,1347,1337],{},[1309,1349,1350],{},"Unchanged; the rule editor gains reference-scale handling in the GUI only.",[196,1352,1354],{"id":1353},"troubleshooting","Troubleshooting",[201,1356,1357,1367,1373,1381,1387,1393],{},[204,1358,1359,1362,1363,1366],{},[207,1360,1361],{},"Nothing renders."," The root rule was given a symbol but no children, or every child's filter matches nothing. Test the expression with ",[18,1364,1365],{},"QgsExpression"," first.",[204,1368,1369,1372],{},[207,1370,1371],{},"Features draw twice."," Two top-level rules match the same feature. That is by design; nest them or tighten the filters if it was not intended.",[204,1374,1375,921,1378,1380],{},[207,1376,1377],{},"The legend entries are blank.",[18,1379,513],{}," was not called.",[204,1382,1383,1386],{},[207,1384,1385],{},"The else rule catches everything."," It was added before its siblings, or the earlier filters are wrong. It must be the last child.",[204,1388,1389,1392],{},[207,1390,1391],{},"A rule never appears however far you zoom."," The scale range is inverted — remember that a larger denominator is a smaller scale.",[204,1394,1395,1398,1399,221],{},[207,1396,1397],{},"The renderer resets after reloading the project."," The layer was styled but the project not saved, or the style was saved to a QML that was not loaded. See ",[26,1400,1402],{"href":1401},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fsave-and-load-qml-style-pyqgis\u002F","Save and Load a QML Style in PyQGIS",[196,1404,1406],{"id":1405},"conclusion","Conclusion",[14,1408,1409,1410,1412],{},"A rule-based renderer is a tree: a symbol-less root, children carrying a filter expression, a symbol and a label, nesting where conditions combine, scale ranges where legibility demands it, and an else rule so nothing vanishes unnoticed. When a categorised renderer already exists, ",[18,1411,1028],{}," turns it into that tree in one call and leaves you free to extend it.",[196,1414,1416],{"id":1415},"frequently-asked-questions","Frequently Asked Questions",[14,1418,1419,1422],{},[207,1420,1421],{},"When should I prefer a rule-based renderer?","\nWhenever the symbology depends on more than one field, on scale, or needs a catch-all. For a single field with distinct values, a categorised renderer is simpler and just as fast.",[14,1424,1425,1428],{},[207,1426,1427],{},"Are rule-based renderers slower?","\nEach rule's expression is evaluated per feature, so a deep tree of complex expressions costs more than a categorised lookup. Keep filters simple, and prefer nesting over repeating a condition in every rule.",[14,1430,1431,1434,1435,1438],{},[207,1432,1433],{},"Can a rule have several symbol layers?","\nYes — the rule's symbol is an ordinary ",[18,1436,1437],{},"QgsSymbol",", so casing, dashes and markers along a line all work as usual.",[14,1440,1441,1444,1445,1448],{},[207,1442,1443],{},"How do I hide a class entirely?","\nGive the rule a filter that matches it and call ",[18,1446,1447],{},"rule.setActive(False)",", or simply omit the rule and let the else rule catch it if you still want it drawn faintly.",[14,1450,1451,1454,1455,221],{},[207,1452,1453],{},"Does the rule tree appear in the layout legend?","\nYes, with its nesting. Labels and grouping are worth tidying before exporting — see ",[26,1456,1458],{"href":1457},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-legend-to-layout-pyqgis\u002F","Add a Legend to a Layout in PyQGIS",[196,1460,1462],{"id":1461},"related","Related",[201,1464,1465,1470,1475,1481,1485],{},[204,1466,1467,1469],{},[26,1468,29],{"href":28}," — the guide this recipe belongs to",[204,1471,1472],{},[26,1473,1474],{"href":1032},"Classify a Layer with Natural Breaks in PyQGIS",[204,1476,1477],{},[26,1478,1480],{"href":1479},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","Create a Choropleth Map in PyQGIS",[204,1482,1483],{},[26,1484,1402],{"href":1401},[204,1486,1487],{},[26,1488,1490],{"href":1489},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis\u002F","Add Rule-Based Labels in PyQGIS",[1492,1493,1494],"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 pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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}",{"title":232,"searchDepth":255,"depth":255,"links":1496},[1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508],{"id":198,"depth":255,"text":199},{"id":224,"depth":255,"text":225},{"id":520,"depth":255,"text":521},{"id":672,"depth":255,"text":673},{"id":850,"depth":255,"text":851},{"id":932,"depth":255,"text":933},{"id":1041,"depth":255,"text":1042},{"id":1276,"depth":255,"text":1277},{"id":1353,"depth":255,"text":1354},{"id":1405,"depth":255,"text":1406},{"id":1415,"depth":255,"text":1416},{"id":1461,"depth":255,"text":1462},"Build QgsRuleBasedRenderer rules in Python — nest rules, set scale ranges, add an else rule, and express symbology that categorised and graduated renderers cannot.","md",{"slug":1512,"type":1513,"breadcrumb":1514,"datePublished":1515,"dateModified":1515},"rule-based-renderer-pyqgis","article","Rule-Based Renderer","2026-08-10","\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis",{"title":5,"description":1509},"pyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002Findex","ucx1ROJ-GDNUMDP92MN9f0_EhiBk7uWWVxE1wpjDm9M",1786401337548]