[{"data":1,"prerenderedAt":1375},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-categorized-renderer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1364,"extension":1365,"meta":1366,"navigation":273,"path":1371,"seo":1372,"stem":1373,"__hash__":1374},"docs\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-categorized-renderer-pyqgis\u002Findex.md","Create a Categorized Renderer in PyQGIS",{"type":7,"value":8,"toc":1351},"minimark",[9,13,17,26,208,213,230,234,500,530,534,537,669,674,779,783,842,852,922,926,929,993,1002,1006,1009,1012,1072,1092,1096,1166,1175,1179,1229,1233,1236,1240,1250,1260,1274,1280,1290,1296,1306,1312,1316,1347],[10,11,5],"h1",{"id":12},"create-a-categorized-renderer-in-pyqgis",[14,15,16],"p",{},"Colouring features by a field is the most common thematic map there is: land use by class, roads by type, sites by status. Doing it by hand is a dialog and a click; doing it from code is what lets a script produce a consistently styled map every night, with the same colour for the same class every time — which is the part that matters, because a legend where \"residential\" is blue on Monday and green on Tuesday is worse than no legend.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","Graduated and Categorized Renderers",". It covers building categories from the data, assigning colours from a ramp or a fixed mapping, labelling and ordering the legend, handling values that appear later, and keeping the result readable when a field has forty distinct values.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,97,101,105,109,115,121,125,135,140,144,148,152,156,160,164,167,171,177,182,186,190,194,200,204],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 272","img","Diagram of a categorized renderer built from unique field values, each mapped to a symbol and a legend label, with an all other values catch-all category","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"How a categorized renderer is assembled",[40,41,42],"desc",{},"The renderer reads a field, finds its unique values, and pairs each with a symbol and a legend label. Features whose value matches a category are drawn with that symbol. A final catch-all category with an empty value draws everything that matches nothing else, which is what prevents new values from appearing as invisible features.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","272","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"catArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#0f766e",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","A category is a value, a symbol and a label",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":66,"style":84},"20","56","200","180","10","#eef7f4","stroke-width:2.5",[68,86,90],{"x":87,"y":88,"style":89,"fill":66,"textAnchor":74},"120","82","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","field values",[68,92,96],{"x":87,"y":93,"style":94,"fill":95,"textAnchor":74},"110","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","residential",[68,98,100],{"x":87,"y":99,"style":94,"fill":95,"textAnchor":74},"134","commercial",[68,102,104],{"x":87,"y":103,"style":94,"fill":95,"textAnchor":74},"158","industrial",[68,106,108],{"x":87,"y":107,"style":94,"fill":95,"textAnchor":74},"182","open space",[68,110,114],{"x":87,"y":111,"style":112,"fill":113,"textAnchor":74},"212","text-anchor:middle;font-size:10px;font-family:sans-serif","#59645f","uniqueValues on the field",[44,116],{"x":117,"y":79,"width":118,"height":81,"rx":82,"fill":119,"stroke":113,"style":120},"268","220","#fffdf7","stroke-width:2",[68,122,124],{"x":123,"y":88,"style":89,"fill":73,"textAnchor":74},"378","categories",[44,126],{"x":127,"y":128,"width":129,"height":130,"rx":131,"fill":132,"fillOpacity":133,"stroke":132,"style":134},"288","96","24","18","3","#2563eb",0.5,"stroke-width:1.5",[68,136,139],{"x":137,"y":93,"style":138,"fill":95},"322","font-size:11px;font-family:sans-serif","Residential",[44,141],{"x":127,"y":142,"width":129,"height":130,"rx":131,"fill":143,"fillOpacity":133,"stroke":143,"style":134},"122","#b45309",[68,145,147],{"x":137,"y":146,"style":138,"fill":95},"136","Commercial",[44,149],{"x":127,"y":150,"width":129,"height":130,"rx":131,"fill":151,"fillOpacity":133,"stroke":151,"style":134},"148","#b91c1c",[68,153,155],{"x":137,"y":154,"style":138,"fill":95},"162","Industrial",[44,157],{"x":127,"y":158,"width":129,"height":130,"rx":131,"fill":159,"fillOpacity":133,"stroke":159,"style":134},"174","#15803d",[68,161,163],{"x":137,"y":162,"style":138,"fill":95},"188","Open space",[44,165],{"x":127,"y":80,"width":129,"height":130,"rx":131,"fill":113,"fillOpacity":166,"stroke":113,"style":134},0.4,[68,168,170],{"x":137,"y":169,"style":138,"fill":95},"214","Other",[44,172],{"x":173,"y":174,"width":175,"height":87,"rx":82,"fill":176,"stroke":132,"style":84},"536","86","204","#eff3ff",[68,178,181],{"x":179,"y":180,"style":89,"fill":132,"textAnchor":74},"638","114","the map",[68,183,185],{"x":179,"y":184,"style":94,"fill":95,"textAnchor":74},"140","each feature drawn",[68,187,189],{"x":179,"y":188,"style":94,"fill":95,"textAnchor":74},"160","with its category symbol",[68,191,193],{"x":179,"y":192,"style":112,"fill":113,"textAnchor":74},"186","and the legend matches",[195,196],"line",{"x1":118,"y1":197,"x2":198,"y2":197,"stroke":66,"style":199},"146","262","stroke-width:2;marker-end:url(#catArrow)",[195,201],{"x1":202,"y1":197,"x2":203,"y2":197,"stroke":66,"style":199},"488","530",[68,205,207],{"x":70,"y":206,"style":94,"fill":113,"textAnchor":74},"260","Without the catch-all, a value added next month draws nothing at all",[209,210,212],"h2",{"id":211},"prerequisites","Prerequisites",[214,215,216,224,227],"ul",{},[217,218,219,223],"li",{},[220,221,222],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[217,225,226],{},"A vector layer with a field holding a manageable number of distinct values.",[217,228,229],{},"The layer loaded in a project, or at least valid — styling applies to the layer object either way.",[209,231,233],{"id":232},"build-categories-from-the-data","Build categories from the data",[235,236,241],"pre",{"className":237,"code":238,"language":239,"meta":240,"style":240},"language-python shiki shiki-themes github-dark","from qgis.core import (QgsProject, QgsCategorizedSymbolRenderer, QgsRendererCategory,\n                       QgsSymbol, QgsStyle)\n\nlayer = QgsProject.instance().mapLayersByName(\"Land use\")[0]\nfield = \"use_class\"\n\nindex = layer.fields().indexOf(field)\nvalues = sorted(v for v in layer.uniqueValues(index) if v)\n\nramp = QgsStyle.defaultStyle().colorRamp(\"Spectral\")\n\ncategories = []\nfor position, value in enumerate(values):\n    symbol = QgsSymbol.defaultSymbol(layer.geometryType())\n    colour = ramp.color(position \u002F max(len(values) - 1, 1))\n    symbol.setColor(colour)\n    categories.append(QgsRendererCategory(value, symbol, str(value)))\n\nlayer.setRenderer(QgsCategorizedSymbolRenderer(field, categories))\nlayer.triggerRepaint()\n","python","",[242,243,244,262,268,275,300,311,316,327,359,364,381,386,397,413,424,465,471,483,488,494],"code",{"__ignoreMap":240},[245,246,248,252,256,259],"span",{"class":195,"line":247},1,[245,249,251],{"class":250},"snl16","from",[245,253,255],{"class":254},"s95oV"," qgis.core ",[245,257,258],{"class":250},"import",[245,260,261],{"class":254}," (QgsProject, QgsCategorizedSymbolRenderer, QgsRendererCategory,\n",[245,263,265],{"class":195,"line":264},2,[245,266,267],{"class":254},"                       QgsSymbol, QgsStyle)\n",[245,269,271],{"class":195,"line":270},3,[245,272,274],{"emptyLinePlaceholder":273},true,"\n",[245,276,278,281,284,287,291,294,297],{"class":195,"line":277},4,[245,279,280],{"class":254},"layer ",[245,282,283],{"class":250},"=",[245,285,286],{"class":254}," QgsProject.instance().mapLayersByName(",[245,288,290],{"class":289},"sU2Wk","\"Land use\"",[245,292,293],{"class":254},")[",[245,295,46],{"class":296},"sDLfK",[245,298,299],{"class":254},"]\n",[245,301,303,306,308],{"class":195,"line":302},5,[245,304,305],{"class":254},"field ",[245,307,283],{"class":250},[245,309,310],{"class":289}," \"use_class\"\n",[245,312,314],{"class":195,"line":313},6,[245,315,274],{"emptyLinePlaceholder":273},[245,317,319,322,324],{"class":195,"line":318},7,[245,320,321],{"class":254},"index ",[245,323,283],{"class":250},[245,325,326],{"class":254}," layer.fields().indexOf(field)\n",[245,328,330,333,335,338,341,344,347,350,353,356],{"class":195,"line":329},8,[245,331,332],{"class":254},"values ",[245,334,283],{"class":250},[245,336,337],{"class":296}," sorted",[245,339,340],{"class":254},"(v ",[245,342,343],{"class":250},"for",[245,345,346],{"class":254}," v ",[245,348,349],{"class":250},"in",[245,351,352],{"class":254}," layer.uniqueValues(index) ",[245,354,355],{"class":250},"if",[245,357,358],{"class":254}," v)\n",[245,360,362],{"class":195,"line":361},9,[245,363,274],{"emptyLinePlaceholder":273},[245,365,367,370,372,375,378],{"class":195,"line":366},10,[245,368,369],{"class":254},"ramp ",[245,371,283],{"class":250},[245,373,374],{"class":254}," QgsStyle.defaultStyle().colorRamp(",[245,376,377],{"class":289},"\"Spectral\"",[245,379,380],{"class":254},")\n",[245,382,384],{"class":195,"line":383},11,[245,385,274],{"emptyLinePlaceholder":273},[245,387,389,392,394],{"class":195,"line":388},12,[245,390,391],{"class":254},"categories ",[245,393,283],{"class":250},[245,395,396],{"class":254}," []\n",[245,398,400,402,405,407,410],{"class":195,"line":399},13,[245,401,343],{"class":250},[245,403,404],{"class":254}," position, value ",[245,406,349],{"class":250},[245,408,409],{"class":296}," enumerate",[245,411,412],{"class":254},"(values):\n",[245,414,416,419,421],{"class":195,"line":415},14,[245,417,418],{"class":254},"    symbol ",[245,420,283],{"class":250},[245,422,423],{"class":254}," QgsSymbol.defaultSymbol(layer.geometryType())\n",[245,425,427,430,432,435,438,441,444,447,450,453,456,459,462],{"class":195,"line":426},15,[245,428,429],{"class":254},"    colour ",[245,431,283],{"class":250},[245,433,434],{"class":254}," ramp.color(position ",[245,436,437],{"class":250},"\u002F",[245,439,440],{"class":296}," max",[245,442,443],{"class":254},"(",[245,445,446],{"class":296},"len",[245,448,449],{"class":254},"(values) ",[245,451,452],{"class":250},"-",[245,454,455],{"class":296}," 1",[245,457,458],{"class":254},", ",[245,460,461],{"class":296},"1",[245,463,464],{"class":254},"))\n",[245,466,468],{"class":195,"line":467},16,[245,469,470],{"class":254},"    symbol.setColor(colour)\n",[245,472,474,477,480],{"class":195,"line":473},17,[245,475,476],{"class":254},"    categories.append(QgsRendererCategory(value, symbol, ",[245,478,479],{"class":296},"str",[245,481,482],{"class":254},"(value)))\n",[245,484,486],{"class":195,"line":485},18,[245,487,274],{"emptyLinePlaceholder":273},[245,489,491],{"class":195,"line":490},19,[245,492,493],{"class":254},"layer.setRenderer(QgsCategorizedSymbolRenderer(field, categories))\n",[245,495,497],{"class":195,"line":496},20,[245,498,499],{"class":254},"layer.triggerRepaint()\n",[14,501,502,505,506,509,510,513,514,517,518,521,522,525,526,529],{},[220,503,504],{},"Breakdown:"," ",[242,507,508],{},"uniqueValues()"," asks the provider for the distinct values, which on a database layer is a ",[242,511,512],{},"SELECT DISTINCT"," rather than a full scan — much faster than iterating features. Filtering out empty values keeps ",[242,515,516],{},"NULL"," and blank strings out of the legend, where they appear as an unlabelled entry nobody can interpret. ",[242,519,520],{},"QgsSymbol.defaultSymbol()"," builds a symbol matching the layer's geometry type, so the same code works for points, lines and polygons. Sampling the ramp by position spreads the colours across it evenly; the ",[242,523,524],{},"max(..., 1)"," guard prevents a division by zero when there is a single category. A ",[242,527,528],{},"QgsRendererCategory"," is exactly its three arguments: the value to match, the symbol, and the legend label.",[209,531,533],{"id":532},"use-a-fixed-colour-mapping-instead","Use a fixed colour mapping instead",[14,535,536],{},"For anything with an established convention — land use classes, road types, a corporate scheme — the colours should not come from a ramp.",[235,538,540],{"className":237,"code":539,"language":239,"meta":240,"style":240},"from qgis.PyQt.QtGui import QColor\n\nCOLOURS = {\n    \"residential\": \"#f6c667\",\n    \"commercial\": \"#8ab4f8\",\n    \"industrial\": \"#c98a8a\",\n    \"open space\": \"#8fc98a\",\n}\n\ncategories = []\nfor value, hex_colour in COLOURS.items():\n    symbol = QgsSymbol.defaultSymbol(layer.geometryType())\n    symbol.setColor(QColor(hex_colour))\n    categories.append(QgsRendererCategory(value, symbol, value.title()))\n",[242,541,542,554,558,569,583,595,607,619,624,628,636,651,659,664],{"__ignoreMap":240},[245,543,544,546,549,551],{"class":195,"line":247},[245,545,251],{"class":250},[245,547,548],{"class":254}," qgis.PyQt.QtGui ",[245,550,258],{"class":250},[245,552,553],{"class":254}," QColor\n",[245,555,556],{"class":195,"line":264},[245,557,274],{"emptyLinePlaceholder":273},[245,559,560,563,566],{"class":195,"line":270},[245,561,562],{"class":296},"COLOURS",[245,564,565],{"class":250}," =",[245,567,568],{"class":254}," {\n",[245,570,571,574,577,580],{"class":195,"line":277},[245,572,573],{"class":289},"    \"residential\"",[245,575,576],{"class":254},": ",[245,578,579],{"class":289},"\"#f6c667\"",[245,581,582],{"class":254},",\n",[245,584,585,588,590,593],{"class":195,"line":302},[245,586,587],{"class":289},"    \"commercial\"",[245,589,576],{"class":254},[245,591,592],{"class":289},"\"#8ab4f8\"",[245,594,582],{"class":254},[245,596,597,600,602,605],{"class":195,"line":313},[245,598,599],{"class":289},"    \"industrial\"",[245,601,576],{"class":254},[245,603,604],{"class":289},"\"#c98a8a\"",[245,606,582],{"class":254},[245,608,609,612,614,617],{"class":195,"line":318},[245,610,611],{"class":289},"    \"open space\"",[245,613,576],{"class":254},[245,615,616],{"class":289},"\"#8fc98a\"",[245,618,582],{"class":254},[245,620,621],{"class":195,"line":329},[245,622,623],{"class":254},"}\n",[245,625,626],{"class":195,"line":361},[245,627,274],{"emptyLinePlaceholder":273},[245,629,630,632,634],{"class":195,"line":366},[245,631,391],{"class":254},[245,633,283],{"class":250},[245,635,396],{"class":254},[245,637,638,640,643,645,648],{"class":195,"line":383},[245,639,343],{"class":250},[245,641,642],{"class":254}," value, hex_colour ",[245,644,349],{"class":250},[245,646,647],{"class":296}," COLOURS",[245,649,650],{"class":254},".items():\n",[245,652,653,655,657],{"class":195,"line":388},[245,654,418],{"class":254},[245,656,283],{"class":250},[245,658,423],{"class":254},[245,660,661],{"class":195,"line":399},[245,662,663],{"class":254},"    symbol.setColor(QColor(hex_colour))\n",[245,665,666],{"class":195,"line":415},[245,667,668],{"class":254},"    categories.append(QgsRendererCategory(value, symbol, value.title()))\n",[14,670,671,673],{},[220,672,504],{}," A dictionary makes the mapping reviewable and, crucially, stable — the same class gets the same colour in every map the script produces, which is the property that lets somebody compare two sheets. Building the categories from the dictionary rather than from the data also means a class that happens to be absent from this extract still appears in the legend, which is usually what a series wants. Where the mapping lives in a file rather than in code, a small CSV or JSON alongside the script keeps cartographic decisions out of the logic.",[14,675,676],{},[29,677,680,683,686,689,692,698,703,709,712,715,721,725,728,731,734,739,743,746,750,754,757,759,762,764,766,768,770,773,776],{"viewBox":678,"role":32,"ariaLabel":679,"xmlns":34},"0 0 760 246","Comparison of colours sampled from a ramp against a fixed mapping, showing what happens to each when the data gains a new class",[36,681,682],{},"What a new class does to your colours",[40,684,685],{},"Colours sampled across a ramp are positioned by how many values are present, so adding a fourth class shifts every colour and two months of maps can no longer be compared. A fixed mapping assigns each class its own colour, so a new class takes a new colour and the existing three are untouched.",[44,687],{"x":46,"y":46,"width":47,"height":688,"fill":49},"246",[68,690,691],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Same data plus one class, two very different legends",[44,693],{"x":78,"y":694,"width":695,"height":696,"rx":82,"fill":697,"stroke":143,"style":84},"52","348","176","#fdf2e2",[68,699,702],{"x":700,"y":701,"style":89,"fill":143,"textAnchor":74},"194","78","sampled from a ramp",[44,704],{"x":705,"y":706,"width":707,"height":129,"rx":131,"fill":132,"fillOpacity":708,"stroke":132,"style":134},"48","94","60",0.45,[44,710],{"x":711,"y":706,"width":707,"height":129,"rx":131,"fill":66,"fillOpacity":708,"stroke":66,"style":134},"116",[44,713],{"x":714,"y":706,"width":707,"height":129,"rx":131,"fill":159,"fillOpacity":708,"stroke":159,"style":134},"184",[68,716,720],{"x":717,"y":718,"style":719,"fill":113},"266","111","font-size:10px;font-family:sans-serif","last month",[44,722],{"x":705,"y":723,"width":724,"height":129,"rx":131,"fill":132,"fillOpacity":708,"stroke":132,"style":134},"132","46",[44,726],{"x":727,"y":723,"width":724,"height":129,"rx":131,"fill":159,"fillOpacity":166,"stroke":159,"style":134},"102",[44,729],{"x":730,"y":723,"width":724,"height":129,"rx":131,"fill":143,"fillOpacity":708,"stroke":143,"style":134},"156",[44,732],{"x":733,"y":723,"width":724,"height":129,"rx":131,"fill":151,"fillOpacity":166,"stroke":151,"style":134},"210",[68,735,738],{"x":736,"y":737,"style":719,"fill":113},"278","149","this month",[68,740,742],{"x":700,"y":162,"style":741,"fill":143,"textAnchor":74},"text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","every colour moved",[68,744,745],{"x":700,"y":733,"style":94,"fill":95,"textAnchor":74},"the two maps cannot be compared",[44,747],{"x":748,"y":694,"width":695,"height":696,"rx":82,"fill":749,"stroke":159,"style":84},"392","#edf8e9",[68,751,753],{"x":752,"y":701,"style":89,"fill":159,"textAnchor":74},"566","fixed mapping",[44,755],{"x":756,"y":706,"width":707,"height":129,"rx":131,"fill":132,"fillOpacity":708,"stroke":132,"style":134},"420",[44,758],{"x":202,"y":706,"width":707,"height":129,"rx":131,"fill":66,"fillOpacity":708,"stroke":66,"style":134},[44,760],{"x":761,"y":706,"width":707,"height":129,"rx":131,"fill":159,"fillOpacity":708,"stroke":159,"style":134},"556",[68,763,720],{"x":179,"y":718,"style":719,"fill":113},[44,765],{"x":756,"y":723,"width":707,"height":129,"rx":131,"fill":132,"fillOpacity":708,"stroke":132,"style":134},[44,767],{"x":202,"y":723,"width":707,"height":129,"rx":131,"fill":66,"fillOpacity":708,"stroke":66,"style":134},[44,769],{"x":761,"y":723,"width":707,"height":129,"rx":131,"fill":159,"fillOpacity":708,"stroke":159,"style":134},[44,771],{"x":772,"y":723,"width":707,"height":129,"rx":131,"fill":143,"fillOpacity":708,"stroke":143,"style":120},"624",[68,774,775],{"x":752,"y":162,"style":741,"fill":159,"textAnchor":74},"three unchanged, one added",[68,777,778],{"x":752,"y":733,"style":94,"fill":95,"textAnchor":74},"the series stays readable",[209,780,782],{"id":781},"add-a-catch-all-for-unexpected-values","Add a catch-all for unexpected values",[235,784,786],{"className":237,"code":785,"language":239,"meta":240,"style":240},"other_symbol = QgsSymbol.defaultSymbol(layer.geometryType())\nother_symbol.setColor(QColor(\"#9aa39f\"))\ncategories.append(QgsRendererCategory(\"\", other_symbol, \"Other\"))\n\nrenderer = QgsCategorizedSymbolRenderer(field, categories)\nlayer.setRenderer(renderer)\n",[242,787,788,797,807,823,827,837],{"__ignoreMap":240},[245,789,790,793,795],{"class":195,"line":247},[245,791,792],{"class":254},"other_symbol ",[245,794,283],{"class":250},[245,796,423],{"class":254},[245,798,799,802,805],{"class":195,"line":264},[245,800,801],{"class":254},"other_symbol.setColor(QColor(",[245,803,804],{"class":289},"\"#9aa39f\"",[245,806,464],{"class":254},[245,808,809,812,815,818,821],{"class":195,"line":270},[245,810,811],{"class":254},"categories.append(QgsRendererCategory(",[245,813,814],{"class":289},"\"\"",[245,816,817],{"class":254},", other_symbol, ",[245,819,820],{"class":289},"\"Other\"",[245,822,464],{"class":254},[245,824,825],{"class":195,"line":277},[245,826,274],{"emptyLinePlaceholder":273},[245,828,829,832,834],{"class":195,"line":302},[245,830,831],{"class":254},"renderer ",[245,833,283],{"class":250},[245,835,836],{"class":254}," QgsCategorizedSymbolRenderer(field, categories)\n",[245,838,839],{"class":195,"line":313},[245,840,841],{"class":254},"layer.setRenderer(renderer)\n",[14,843,844,846,847,851],{},[220,845,504],{}," A category with an empty value acts as the fallback for anything not matched by another category. Without it, a value that appears in the data after the style was written is drawn with ",[848,849,850],"em",{},"nothing"," — the features are simply invisible, with no warning, which is one of the more dangerous silent failures in cartography because the map looks fine. A neutral grey labelled \"Other\" makes the gap visible instead. In a scheduled job it is worth going further and logging when the catch-all matches anything, since that is the signal that the classification needs updating.",[14,853,854],{},[29,855,858,861,864,867,870,872,876,882,884,888,891,895,899,901,904,907,910,913,916,919],{"viewBox":856,"role":32,"ariaLabel":857,"xmlns":34},"0 0 760 250","Comparison of a map without a catch-all category where new values are invisible against one with a catch-all where they appear in grey and are visible in the legend",[36,859,860],{},"What a missing category looks like",[40,862,863],{},"Without a catch-all, features whose class was added after the style was written are drawn with no symbol, so a district silently disappears from the map and nothing in the legend suggests anything is missing. With a catch-all, the same features are drawn in neutral grey and labelled Other, making the gap obvious and fixable.",[44,865],{"x":46,"y":46,"width":47,"height":866,"fill":49},"250",[68,868,869],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"A map with a hole looks exactly like a map",[44,871],{"x":78,"y":705,"width":695,"height":81,"rx":82,"fill":697,"stroke":151,"style":84},[68,873,875],{"x":700,"y":874,"style":89,"fill":151,"textAnchor":74},"74","no catch-all",[44,877],{"x":694,"y":878,"width":87,"height":879,"rx":880,"fill":132,"fillOpacity":881,"stroke":132,"style":134},"92","80","4",0.35,[44,883],{"x":81,"y":878,"width":879,"height":879,"rx":880,"fill":143,"fillOpacity":881,"stroke":143,"style":134},[44,885],{"x":117,"y":878,"width":886,"height":879,"rx":880,"fill":49,"stroke":151,"style":887},"72","stroke-width:1.5;stroke-dasharray:5 4",[68,889,850],{"x":890,"y":146,"style":112,"fill":151,"textAnchor":74},"304",[68,892,894],{"x":700,"y":893,"style":94,"fill":95,"textAnchor":74},"198","a new class draws no symbol",[68,896,898],{"x":700,"y":897,"style":741,"fill":151,"textAnchor":74},"216","and the legend says nothing",[44,900],{"x":748,"y":705,"width":695,"height":81,"rx":82,"fill":749,"stroke":159,"style":84},[68,902,903],{"x":752,"y":874,"style":89,"fill":159,"textAnchor":74},"with a catch-all",[44,905],{"x":906,"y":878,"width":87,"height":879,"rx":880,"fill":132,"fillOpacity":881,"stroke":132,"style":134},"424",[44,908],{"x":909,"y":878,"width":879,"height":879,"rx":880,"fill":143,"fillOpacity":881,"stroke":143,"style":134},"552",[44,911],{"x":912,"y":878,"width":886,"height":879,"rx":880,"fill":113,"fillOpacity":881,"stroke":113,"style":134},"640",[68,914,170],{"x":915,"y":146,"style":112,"fill":95,"textAnchor":74},"676",[68,917,918],{"x":752,"y":893,"style":94,"fill":95,"textAnchor":74},"drawn in grey, listed in the legend",[68,920,921],{"x":752,"y":897,"style":741,"fill":159,"textAnchor":74},"somebody notices and fixes it",[209,923,925],{"id":924},"order-and-label-the-legend","Order and label the legend",[14,927,928],{},"The legend reads in category order, which is the order you built them in — and that is rarely the order a reader wants.",[235,930,932],{"className":237,"code":931,"language":239,"meta":240,"style":240},"renderer = layer.renderer()\n\nordered = sorted(renderer.categories(),\n                 key=lambda c: (c.value() == \"\", str(c.label())))\n\nlayer.setRenderer(QgsCategorizedSymbolRenderer(field, ordered))\n",[242,933,934,943,947,959,984,988],{"__ignoreMap":240},[245,935,936,938,940],{"class":195,"line":247},[245,937,831],{"class":254},[245,939,283],{"class":250},[245,941,942],{"class":254}," layer.renderer()\n",[245,944,945],{"class":195,"line":264},[245,946,274],{"emptyLinePlaceholder":273},[245,948,949,952,954,956],{"class":195,"line":270},[245,950,951],{"class":254},"ordered ",[245,953,283],{"class":250},[245,955,337],{"class":296},[245,957,958],{"class":254},"(renderer.categories(),\n",[245,960,961,965,968,971,974,977,979,981],{"class":195,"line":277},[245,962,964],{"class":963},"s9osk","                 key",[245,966,967],{"class":250},"=lambda",[245,969,970],{"class":254}," c: (c.value() ",[245,972,973],{"class":250},"==",[245,975,976],{"class":289}," \"\"",[245,978,458],{"class":254},[245,980,479],{"class":296},[245,982,983],{"class":254},"(c.label())))\n",[245,985,986],{"class":195,"line":302},[245,987,274],{"emptyLinePlaceholder":273},[245,989,990],{"class":195,"line":313},[245,991,992],{"class":254},"layer.setRenderer(QgsCategorizedSymbolRenderer(field, ordered))\n",[14,994,995,997,998,1001],{},[220,996,504],{}," Sorting by label puts the legend in alphabetical order, and the leading tuple element pushes the catch-all to the bottom where it belongs — a legend ending in \"Other\" reads correctly, one starting with it does not. Where categories have a natural sequence that is not alphabetical, sort by an explicit ordering list instead: land-use classes usually want a designed order, not an accidental one. Labels are independent of values, which is what lets a database code such as ",[242,999,1000],{},"R1"," appear in the legend as \"Residential — low density\" without touching the data.",[209,1003,1005],{"id":1004},"keep-it-readable-when-there-are-too-many-values","Keep it readable when there are too many values",[14,1007,1008],{},"Beyond about eight categories a legend stops being interpretable: nobody can hold twelve colours in mind while reading a map, and adjacent hues become indistinguishable at the size a category patch is printed.",[14,1010,1011],{},"The fix is nearly always in the data rather than the renderer. Group the values into a smaller classification — either with an expression as the renderer's attribute, or by adding a grouped field:",[235,1013,1015],{"className":237,"code":1014,"language":239,"meta":240,"style":240},"renderer = QgsCategorizedSymbolRenderer(\n    \"CASE \"\n    \"WHEN \\\"use_class\\\" IN ('R1', 'R2', 'R3') THEN 'Residential' \"\n    \"WHEN \\\"use_class\\\" IN ('C1', 'C2') THEN 'Commercial' \"\n    \"ELSE 'Other' END\",\n    categories)\n",[242,1016,1017,1026,1031,1047,1060,1067],{"__ignoreMap":240},[245,1018,1019,1021,1023],{"class":195,"line":247},[245,1020,831],{"class":254},[245,1022,283],{"class":250},[245,1024,1025],{"class":254}," QgsCategorizedSymbolRenderer(\n",[245,1027,1028],{"class":195,"line":264},[245,1029,1030],{"class":289},"    \"CASE \"\n",[245,1032,1033,1036,1039,1042,1044],{"class":195,"line":270},[245,1034,1035],{"class":289},"    \"WHEN ",[245,1037,1038],{"class":296},"\\\"",[245,1040,1041],{"class":289},"use_class",[245,1043,1038],{"class":296},[245,1045,1046],{"class":289}," IN ('R1', 'R2', 'R3') THEN 'Residential' \"\n",[245,1048,1049,1051,1053,1055,1057],{"class":195,"line":277},[245,1050,1035],{"class":289},[245,1052,1038],{"class":296},[245,1054,1041],{"class":289},[245,1056,1038],{"class":296},[245,1058,1059],{"class":289}," IN ('C1', 'C2') THEN 'Commercial' \"\n",[245,1061,1062,1065],{"class":195,"line":302},[245,1063,1064],{"class":289},"    \"ELSE 'Other' END\"",[245,1066,582],{"class":254},[245,1068,1069],{"class":195,"line":313},[245,1070,1071],{"class":254},"    categories)\n",[14,1073,1074,1076,1077,1080,1081,1083,1084,1086,1087,1091],{},[220,1075,504],{}," The renderer's attribute can be a full QGIS expression rather than a field name, which means the grouping lives in the style and needs no change to the data — useful when the source is read-only or shared. The categories then match the expression's ",[848,1078,1079],{},"output"," values, so they are ",[242,1082,139],{}," and ",[242,1085,147],{}," rather than the codes. The trade is performance: an expression is evaluated per feature at render time, so on a very large layer a materialised field is faster. The expression language is covered in ",[21,1088,1090],{"href":1089},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","Working with QGIS Expressions",".",[209,1093,1095],{"id":1094},"qgis-version-compatibility","QGIS version compatibility",[1097,1098,1099,1115],"table",{},[1100,1101,1102],"thead",{},[1103,1104,1105,1109,1112],"tr",{},[1106,1107,1108],"th",{},"QGIS version",[1106,1110,1111],{},"Python",[1106,1113,1114],{},"Notes",[1116,1117,1118,1135,1145,1156],"tbody",{},[1103,1119,1120,1124,1127],{},[1121,1122,1123],"td",{},"3.22 LTR",[1121,1125,1126],{},"3.9",[1121,1128,1129,1083,1132,1134],{},[242,1130,1131],{},"QgsCategorizedSymbolRenderer",[242,1133,528],{}," as shown.",[1103,1136,1137,1140,1142],{},[1121,1138,1139],{},"3.28 LTR",[1121,1141,1126],{},[1121,1143,1144],{},"Identical.",[1103,1146,1147,1150,1153],{},[1121,1148,1149],{},"3.34 LTR",[1121,1151,1152],{},"3.12",[1121,1154,1155],{},"Baseline for this page.",[1103,1157,1158,1161,1163],{},[1121,1159,1160],{},"3.40 \u002F 3.44",[1121,1162,1152],{},[1121,1164,1165],{},"Identical; the categorized panel gained value-matching helpers that do not affect the API.",[14,1167,1168,1170,1171,1174],{},[242,1169,520],{}," takes the layer's geometry type, which is a scoped enumeration on newer releases — passing ",[242,1172,1173],{},"layer.geometryType()"," directly is correct on all of them and avoids the question.",[209,1176,1178],{"id":1177},"troubleshooting","Troubleshooting",[214,1180,1181,1187,1193,1203,1211,1223],{},[217,1182,1183,1186],{},[220,1184,1185],{},"Some features are invisible."," Their value matches no category. Add the catch-all.",[217,1188,1189,1192],{},[220,1190,1191],{},"The legend shows raw codes."," The label defaults to the value. Pass a readable label as the third argument.",[217,1194,1195,1198,1199,1202],{},[220,1196,1197],{},"All categories are the same colour."," The symbol object was reused across categories — each needs its own, since ",[242,1200,1201],{},"setColor()"," mutates it.",[217,1204,1205,1210],{},[220,1206,1207,1209],{},[242,1208,508],{}," is slow."," The layer is large and file-based. Restrict with a subset string first, or read the distinct values from the source database.",[217,1212,1213,505,1216,1219,1220,1091],{},[220,1214,1215],{},"The style does not appear.",[242,1217,1218],{},"triggerRepaint()"," was not called, or the renderer was built and never assigned with ",[242,1221,1222],{},"setRenderer()",[217,1224,1225,1228],{},[220,1226,1227],{},"Colours change between runs."," The ramp is sampled over a value list whose length varies. Use a fixed mapping for anything comparable.",[209,1230,1232],{"id":1231},"conclusion","Conclusion",[14,1234,1235],{},"Build a category per value with its own symbol and a readable label, colour them from a ramp for exploration and from a fixed mapping for anything published, and always add a catch-all so a new value is grey rather than invisible. Sort the legend deliberately, and when a field has too many values group them with an expression rather than shipping a legend nobody can read.",[209,1237,1239],{"id":1238},"frequently-asked-questions","Frequently Asked Questions",[14,1241,1242,1245,1246,1091],{},[220,1243,1244],{},"How is this different from a graduated renderer?","\nCategorized matches discrete values; graduated splits a continuous number into ranges — see ",[21,1247,1249],{"href":1248},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis\u002F","Classify a Layer with Natural Breaks in PyQGIS",[14,1251,1252,1255,1256,1259],{},[220,1253,1254],{},"Can I categorize on more than one field?","\nNot directly, but the attribute can be an expression combining fields, such as ",[242,1257,1258],{},"\"class\" || ' - ' || \"status\"",". For genuinely multi-dimensional symbology, a rule-based renderer is the better tool.",[14,1261,1262,1265,1266,1269,1270,1273],{},[220,1263,1264],{},"How do I change one category's colour afterwards?","\nFind its index with ",[242,1267,1268],{},"renderer.categoryIndexForValue()",", then call ",[242,1271,1272],{},"renderer.updateCategorySymbol()"," with a new symbol.",[14,1275,1276,1279],{},[220,1277,1278],{},"Does the order of categories affect drawing?","\nYes — later categories draw over earlier ones where features overlap, which matters for overlapping polygons and for points.",[14,1281,1282,1285,1286,1091],{},[220,1283,1284],{},"Can I save this style for reuse?","\nYes, as a QML file — see ",[21,1287,1289],{"href":1288},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fsave-and-load-qml-style-pyqgis\u002F","Save and Load a QML Style in PyQGIS",[14,1291,1292,1295],{},[220,1293,1294],{},"How do I keep the same colours when the data gains a class?","\nUse a fixed mapping rather than a sampled ramp. A ramp sampled across however many values happen to be present reassigns every colour the moment the count changes, which makes two months' maps incomparable.",[14,1297,1298,1301,1302,1305],{},[220,1299,1300],{},"Can categories use a symbol other than the default?","\nYes — build any symbol you like and pass it to the category, including one loaded from the style library with ",[242,1303,1304],{},"QgsStyle.defaultStyle().symbol(name)",". That is how an organisation's standard symbols end up in a scripted map.",[14,1307,1308,1311],{},[220,1309,1310],{},"Why do my colours look different in the layout?","\nLayouts render at a different resolution and may use a different colour profile. Check the export settings rather than the renderer.",[209,1313,1315],{"id":1314},"related","Related",[214,1317,1318,1323,1329,1335,1341],{},[217,1319,1320,1322],{},[21,1321,24],{"href":23}," — the guide this recipe belongs to",[217,1324,1325],{},[21,1326,1328],{"href":1327},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","Create a Choropleth Map in PyQGIS",[217,1330,1331],{},[21,1332,1334],{"href":1333},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002F","Create a Rule-Based Renderer in PyQGIS",[217,1336,1337],{},[21,1338,1340],{"href":1339},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fcopy-style-between-layers-pyqgis\u002F","Copy a Style Between Layers in PyQGIS",[217,1342,1343],{},[21,1344,1346],{"href":1345},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fset-vector-layer-symbol-color-pyqgis\u002F","Set a Vector Layer Symbol Colour in PyQGIS",[1348,1349,1350],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":240,"searchDepth":264,"depth":264,"links":1352},[1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363],{"id":211,"depth":264,"text":212},{"id":232,"depth":264,"text":233},{"id":532,"depth":264,"text":533},{"id":781,"depth":264,"text":782},{"id":924,"depth":264,"text":925},{"id":1004,"depth":264,"text":1005},{"id":1094,"depth":264,"text":1095},{"id":1177,"depth":264,"text":1178},{"id":1231,"depth":264,"text":1232},{"id":1238,"depth":264,"text":1239},{"id":1314,"depth":264,"text":1315},"Colour a layer by a field from code — build categories from unique values, apply a colour ramp, label and order them, handle the \"all other values\" case, and keep the legend readable.","md",{"slug":1367,"type":1368,"breadcrumb":1369,"datePublished":1370,"dateModified":1370},"create-categorized-renderer-pyqgis","article","Categorized Renderer","2026-08-15","\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-categorized-renderer-pyqgis",{"title":5,"description":1364},"pyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-categorized-renderer-pyqgis\u002Findex","NsdqOhee2QIGixBRGD4h2CaxJN4vLlKOa_vh5aCtPeU",1786789584609]