[{"data":1,"prerenderedAt":1631},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fregister-custom-expression-function-pyqgis":3},{"id":4,"title":5,"body":6,"description":1620,"extension":1621,"meta":1622,"navigation":270,"path":1627,"seo":1628,"stem":1629,"__hash__":1630},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fregister-custom-expression-function-pyqgis\u002Findex.md","Register a Custom Expression Function in PyQGIS",{"type":7,"value":8,"toc":1607},"minimark",[9,13,22,35,201,206,231,235,238,411,428,445,535,555,559,565,722,749,753,760,918,946,957,961,964,1064,1142,1154,1158,1161,1282,1285,1332,1347,1351,1357,1423,1426,1430,1508,1512,1530,1534,1549,1563,1572,1578,1582,1603],[10,11,5],"h1",{"id":12},"register-a-custom-expression-function-in-pyqgis",[14,15,16,17,21],"p",{},"The QGIS expression library is broad, but it is not your domain. When the same twelve-line ",[18,19,20],"code",{},"CASE WHEN"," block keeps reappearing in your labelling rules, or the calculation you need genuinely cannot be expressed — a lookup against a reference table, a checksum, a call to a local library — you can add a Python function to the engine. From the moment it is registered it behaves like any built-in: it appears in the expression builder dialog, works in the field calculator, and can be used in a data-defined property.",[14,23,24,25,30,31,34],{},"This page is a focused recipe within ",[26,27,29],"a",{"href":28},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","Working with QGIS Expressions in PyQGIS",". It covers the ",[18,32,33],{},"@qgsfunction"," decorator, the mandatory parameters, declaring which columns the function reads, and the unregistration discipline that keeps a plugin reloadable.",[14,36,37],{},[38,39,44,48,52,59,76,85,95,101,106,110,118,123,127,130,138,143,146,150,153,157,160,164,171,178,182,185,189,195],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 250","img","A decorated Python function is registered into the shared expression function registry, from where the field calculator, labelling rules, filters and data-defined properties can all call it","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"Where a registered function becomes available",[49,50,51],"desc",{},"A Python function decorated with qgsfunction is added to the QGIS expression function registry. Once registered, four consumers can call it: the field calculator, rule-based labelling, feature filters, and data-defined symbol properties. A dashed return arrow marks unregisterFunction, which must run on plugin unload.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","250","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"fnRegArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Register once, callable from everywhere an expression is accepted",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"16","82","184","86","10","#26322d","#0f766e","stroke-width:2.5",[77,96,33],{"x":97,"y":98,"style":99,"fill":100,"textAnchor":83},"108","110","text-anchor:middle;font-size:12.5px;font-weight:bold;font-family:sans-serif","#d9f99d",[77,102,105],{"x":97,"y":103,"style":104,"fill":100,"textAnchor":83},"132","text-anchor:middle;font-size:11.5px;font-family:sans-serif","def utm_zone(lon,",[77,107,109],{"x":97,"y":108,"style":104,"fill":100,"textAnchor":83},"150","feature, parent):",[53,111],{"x":112,"y":113,"width":114,"height":115,"rx":91,"fill":116,"stroke":117,"style":94},"256","76","196","98","#fffdf7","#2563eb",[77,119,122],{"x":120,"y":121,"style":99,"fill":82,"textAnchor":83},"354","106","function registry",[77,124,126],{"x":120,"y":125,"style":104,"fill":75,"textAnchor":83},"130","process-wide, shared",[77,128,129],{"x":120,"y":108,"style":104,"fill":117,"textAnchor":83},"names must be unique",[53,131],{"x":132,"y":133,"width":134,"height":135,"rx":67,"fill":116,"stroke":136,"style":137},"512","44","232","38","#15803d","stroke-width:2",[77,139,142],{"x":140,"y":141,"style":104,"fill":75,"textAnchor":83},"628","68","field calculator",[53,144],{"x":132,"y":145,"width":134,"height":135,"rx":67,"fill":116,"stroke":136,"style":137},"90",[77,147,149],{"x":140,"y":148,"style":104,"fill":75,"textAnchor":83},"114","rule-based labelling",[53,151],{"x":132,"y":152,"width":134,"height":135,"rx":67,"fill":116,"stroke":136,"style":137},"136",[77,154,156],{"x":140,"y":155,"style":104,"fill":75,"textAnchor":83},"160","feature filters",[53,158],{"x":132,"y":159,"width":134,"height":135,"rx":67,"fill":116,"stroke":136,"style":137},"182",[77,161,163],{"x":140,"y":162,"style":104,"fill":75,"textAnchor":83},"206","data-defined properties",[165,166],"line",{"x1":167,"y1":168,"x2":169,"y2":168,"stroke":75,"style":170},"200","125","252","stroke-width:2.5;marker-end:url(#fnRegArrow)",[165,172],{"x1":173,"y1":174,"x2":175,"y2":176,"stroke":136,"style":177},"452","112","508","63","stroke-width:2;marker-end:url(#fnRegArrow)",[165,179],{"x1":173,"y1":180,"x2":175,"y2":181,"stroke":136,"style":177},"118","109",[165,183],{"x1":173,"y1":103,"x2":175,"y2":184,"stroke":136,"style":177},"155",[165,186],{"x1":173,"y1":187,"x2":175,"y2":188,"stroke":136,"style":177},"140","201",[72,190],{"d":191,"fill":192,"stroke":193,"style":194},"M300 174 Q300 214 200 214 Q120 214 120 172","none","#b91c1c","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#fnRegArrow)",[77,196,200],{"x":197,"y":198,"style":199,"fill":193,"textAnchor":83},"215","234","text-anchor:middle;font-size:11px;font-family:sans-serif","unregisterFunction() on unload",[202,203,205],"h2",{"id":204},"prerequisites","Prerequisites",[207,208,209,217,225],"ul",{},[210,211,212,216],"li",{},[213,214,215],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[210,218,219,220,224],{},"The Python Console for experimentation, or a plugin skeleton for anything permanent — see ",[26,221,223],{"href":222},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate & Structure",".",[210,226,227,228,224],{},"An understanding of how the engine resolves names, since a custom function participates in the same ",[26,229,230],{"href":28},"scope stack",[202,232,234],{"id":233},"write-and-register-a-function","Write and register a function",[14,236,237],{},"The decorator does the registration. Two trailing parameters are mandatory and are supplied by the engine rather than the caller.",[239,240,245],"pre",{"className":241,"code":242,"language":243,"meta":244,"style":244},"language-python shiki shiki-themes github-dark","from qgis.core import qgsfunction\n\n\n@qgsfunction(args=\"auto\", group=\"Custom\", referenced_columns=[])\ndef utm_zone(longitude, feature, parent):\n    \"\"\"\n    Return the UTM zone number containing a longitude.\n\n    \u003Ch4>Syntax\u003C\u002Fh4>\n    \u003Cp>utm_zone(\u003Ci>longitude\u003C\u002Fi>)\u003C\u002Fp>\n    \u003Ch4>Example\u003C\u002Fh4>\n    \u003Cp>utm_zone(12.49) &rarr; 33\u003C\u002Fp>\n    \"\"\"\n    return int((longitude + 180) \u002F 6) + 1\n","python","",[18,246,247,265,272,277,318,330,336,342,347,353,359,365,371,376],{"__ignoreMap":244},[248,249,251,255,259,262],"span",{"class":165,"line":250},1,[248,252,254],{"class":253},"snl16","from",[248,256,258],{"class":257},"s95oV"," qgis.core ",[248,260,261],{"class":253},"import",[248,263,264],{"class":257}," qgsfunction\n",[248,266,268],{"class":165,"line":267},2,[248,269,271],{"emptyLinePlaceholder":270},true,"\n",[248,273,275],{"class":165,"line":274},3,[248,276,271],{"emptyLinePlaceholder":270},[248,278,280,283,286,290,293,297,300,303,305,308,310,313,315],{"class":165,"line":279},4,[248,281,33],{"class":282},"svObZ",[248,284,285],{"class":257},"(",[248,287,289],{"class":288},"s9osk","args",[248,291,292],{"class":253},"=",[248,294,296],{"class":295},"sU2Wk","\"auto\"",[248,298,299],{"class":257},", ",[248,301,302],{"class":288},"group",[248,304,292],{"class":253},[248,306,307],{"class":295},"\"Custom\"",[248,309,299],{"class":257},[248,311,312],{"class":288},"referenced_columns",[248,314,292],{"class":253},[248,316,317],{"class":257},"[])\n",[248,319,321,324,327],{"class":165,"line":320},5,[248,322,323],{"class":253},"def",[248,325,326],{"class":282}," utm_zone",[248,328,329],{"class":257},"(longitude, feature, parent):\n",[248,331,333],{"class":165,"line":332},6,[248,334,335],{"class":295},"    \"\"\"\n",[248,337,339],{"class":165,"line":338},7,[248,340,341],{"class":295},"    Return the UTM zone number containing a longitude.\n",[248,343,345],{"class":165,"line":344},8,[248,346,271],{"emptyLinePlaceholder":270},[248,348,350],{"class":165,"line":349},9,[248,351,352],{"class":295},"    \u003Ch4>Syntax\u003C\u002Fh4>\n",[248,354,356],{"class":165,"line":355},10,[248,357,358],{"class":295},"    \u003Cp>utm_zone(\u003Ci>longitude\u003C\u002Fi>)\u003C\u002Fp>\n",[248,360,362],{"class":165,"line":361},11,[248,363,364],{"class":295},"    \u003Ch4>Example\u003C\u002Fh4>\n",[248,366,368],{"class":165,"line":367},12,[248,369,370],{"class":295},"    \u003Cp>utm_zone(12.49) &rarr; 33\u003C\u002Fp>\n",[248,372,374],{"class":165,"line":373},13,[248,375,335],{"class":295},[248,377,379,382,386,389,392,395,398,401,404,406,408],{"class":165,"line":378},14,[248,380,381],{"class":253},"    return",[248,383,385],{"class":384},"sDLfK"," int",[248,387,388],{"class":257},"((longitude ",[248,390,391],{"class":253},"+",[248,393,394],{"class":384}," 180",[248,396,397],{"class":257},") ",[248,399,400],{"class":253},"\u002F",[248,402,403],{"class":384}," 6",[248,405,397],{"class":257},[248,407,391],{"class":253},[248,409,410],{"class":384}," 1\n",[14,412,413,416,417,420,421,423,424,427],{},[213,414,415],{},"Breakdown:"," ",[18,418,419],{},"args=\"auto\""," tells QGIS to count the arguments from the signature, excluding the two trailing ones — so this function takes exactly one argument in an expression. ",[18,422,302],{}," decides which folder it appears under in the expression builder. ",[18,425,426],{},"referenced_columns=[]"," declares that the function reads no fields itself, which lets the provider keep optimising attribute fetching; omit it and QGIS conservatively assumes every column might be needed. The docstring becomes the help panel in the dialog and accepts a little HTML, which is worth using — a function with no help text is one nobody else will adopt.",[14,429,430,431,434,435,438,439,441,442,444],{},"The ",[18,432,433],{},"feature"," and ",[18,436,437],{},"parent"," parameters are always last. ",[18,440,433],{}," is the feature currently being evaluated, which lets a function reach attributes the caller did not pass. ",[18,443,437],{}," is the expression node, used to report errors:",[239,446,448],{"className":241,"code":447,"language":243,"meta":244,"style":244},"@qgsfunction(args=\"auto\", group=\"Custom\", referenced_columns=[])\ndef safe_ratio(numerator, denominator, feature, parent):\n    \"\"\"Divide two numbers, reporting a clear error instead of raising.\"\"\"\n    if not denominator:\n        parent.setEvalErrorString(\"safe_ratio: denominator is zero or NULL\")\n        return None\n    return numerator \u002F denominator\n",[18,449,450,478,488,493,504,515,523],{"__ignoreMap":244},[248,451,452,454,456,458,460,462,464,466,468,470,472,474,476],{"class":165,"line":250},[248,453,33],{"class":282},[248,455,285],{"class":257},[248,457,289],{"class":288},[248,459,292],{"class":253},[248,461,296],{"class":295},[248,463,299],{"class":257},[248,465,302],{"class":288},[248,467,292],{"class":253},[248,469,307],{"class":295},[248,471,299],{"class":257},[248,473,312],{"class":288},[248,475,292],{"class":253},[248,477,317],{"class":257},[248,479,480,482,485],{"class":165,"line":267},[248,481,323],{"class":253},[248,483,484],{"class":282}," safe_ratio",[248,486,487],{"class":257},"(numerator, denominator, feature, parent):\n",[248,489,490],{"class":165,"line":274},[248,491,492],{"class":295},"    \"\"\"Divide two numbers, reporting a clear error instead of raising.\"\"\"\n",[248,494,495,498,501],{"class":165,"line":279},[248,496,497],{"class":253},"    if",[248,499,500],{"class":253}," not",[248,502,503],{"class":257}," denominator:\n",[248,505,506,509,512],{"class":165,"line":320},[248,507,508],{"class":257},"        parent.setEvalErrorString(",[248,510,511],{"class":295},"\"safe_ratio: denominator is zero or NULL\"",[248,513,514],{"class":257},")\n",[248,516,517,520],{"class":165,"line":332},[248,518,519],{"class":253},"        return",[248,521,522],{"class":384}," None\n",[248,524,525,527,530,532],{"class":165,"line":338},[248,526,381],{"class":253},[248,528,529],{"class":257}," numerator ",[248,531,400],{"class":253},[248,533,534],{"class":257}," denominator\n",[14,536,537,416,539,542,543,546,547,550,551,554],{},[213,538,415],{},[18,540,541],{},"parent.setEvalErrorString()"," routes the message through the engine's normal evaluation-error channel, so ",[18,544,545],{},"expression.evalErrorString()"," picks it up exactly as it would for a built-in failure. Returning ",[18,548,549],{},"None"," alongside it yields ",[18,552,553],{},"NULL"," in the expression, which is the engine's convention for \"no answer\". Raising a Python exception here instead would surface as an opaque error with no context about which feature caused it.",[202,556,558],{"id":557},"read-fields-inside-the-function","Read fields inside the function",[14,560,561,562,564],{},"When a function needs a column the caller did not pass, take it from ",[18,563,433],{}," — and declare it, so the provider still knows to fetch it.",[239,566,568],{"className":241,"code":567,"language":243,"meta":244,"style":244},"@qgsfunction(args=\"auto\", group=\"Custom\", referenced_columns=[\"zoning\", \"assessed_value\"])\ndef rate_band(feature, parent):\n    \"\"\"Classify a parcel into a rating band from its zoning and value.\"\"\"\n    zoning = feature[\"zoning\"]\n    value = feature[\"assessed_value\"] or 0\n    if zoning == \"commercial\":\n        return \"A\" if value > 1_000_000 else \"B\"\n    return \"C\" if value > 400_000 else \"D\"\n",[18,569,570,610,620,625,640,660,676,701],{"__ignoreMap":244},[248,571,572,574,576,578,580,582,584,586,588,590,592,594,596,599,602,604,607],{"class":165,"line":250},[248,573,33],{"class":282},[248,575,285],{"class":257},[248,577,289],{"class":288},[248,579,292],{"class":253},[248,581,296],{"class":295},[248,583,299],{"class":257},[248,585,302],{"class":288},[248,587,292],{"class":253},[248,589,307],{"class":295},[248,591,299],{"class":257},[248,593,312],{"class":288},[248,595,292],{"class":253},[248,597,598],{"class":257},"[",[248,600,601],{"class":295},"\"zoning\"",[248,603,299],{"class":257},[248,605,606],{"class":295},"\"assessed_value\"",[248,608,609],{"class":257},"])\n",[248,611,612,614,617],{"class":165,"line":267},[248,613,323],{"class":253},[248,615,616],{"class":282}," rate_band",[248,618,619],{"class":257},"(feature, parent):\n",[248,621,622],{"class":165,"line":274},[248,623,624],{"class":295},"    \"\"\"Classify a parcel into a rating band from its zoning and value.\"\"\"\n",[248,626,627,630,632,635,637],{"class":165,"line":279},[248,628,629],{"class":257},"    zoning ",[248,631,292],{"class":253},[248,633,634],{"class":257}," feature[",[248,636,601],{"class":295},[248,638,639],{"class":257},"]\n",[248,641,642,645,647,649,651,654,657],{"class":165,"line":320},[248,643,644],{"class":257},"    value ",[248,646,292],{"class":253},[248,648,634],{"class":257},[248,650,606],{"class":295},[248,652,653],{"class":257},"] ",[248,655,656],{"class":253},"or",[248,658,659],{"class":384}," 0\n",[248,661,662,664,667,670,673],{"class":165,"line":332},[248,663,497],{"class":253},[248,665,666],{"class":257}," zoning ",[248,668,669],{"class":253},"==",[248,671,672],{"class":295}," \"commercial\"",[248,674,675],{"class":257},":\n",[248,677,678,680,683,686,689,692,695,698],{"class":165,"line":338},[248,679,519],{"class":253},[248,681,682],{"class":295}," \"A\"",[248,684,685],{"class":253}," if",[248,687,688],{"class":257}," value ",[248,690,691],{"class":253},">",[248,693,694],{"class":384}," 1_000_000",[248,696,697],{"class":253}," else",[248,699,700],{"class":295}," \"B\"\n",[248,702,703,705,708,710,712,714,717,719],{"class":165,"line":344},[248,704,381],{"class":253},[248,706,707],{"class":295}," \"C\"",[248,709,685],{"class":253},[248,711,688],{"class":257},[248,713,691],{"class":253},[248,715,716],{"class":384}," 400_000",[248,718,697],{"class":253},[248,720,721],{"class":295}," \"D\"\n",[14,723,724,726,727,730,731,434,733,735,736,738,739,741,742,745,746,748],{},[213,725,415],{}," This function takes no expression arguments at all — ",[18,728,729],{},"rate_band()"," is how it is called — so ",[18,732,433],{},[18,734,437],{}," are the entire signature. ",[18,737,312],{}," now lists the two fields it reads. Getting that list wrong is subtle: an under-declared column may still work in the field calculator, which fetches everything, and then return ",[18,740,553],{}," in a labelling rule, which fetches only what was declared. The ",[18,743,744],{},"or 0"," guard is the usual ",[18,747,553],{}," defence.",[202,750,752],{"id":751},"register-without-the-decorator","Register without the decorator",[14,754,755,756,759],{},"Inside a plugin, registering at import time is the wrong moment — it happens before the plugin is properly initialised and cannot be undone cleanly. Register explicitly in ",[18,757,758],{},"initGui()"," instead.",[239,761,763],{"className":241,"code":762,"language":243,"meta":244,"style":244},"from qgis.core import QgsExpression, qgsfunction\n\n\n@qgsfunction(args=\"auto\", group=\"Custom\", referenced_columns=[], register=False)\ndef utm_zone(longitude, feature, parent):\n    \"\"\"Return the UTM zone number containing a longitude.\"\"\"\n    return int((longitude + 180) \u002F 6) + 1\n\n\nclass MyPlugin:\n    def initGui(self):\n        QgsExpression.registerFunction(utm_zone)\n\n    def unload(self):\n        QgsExpression.unregisterFunction(\"utm_zone\")\n",[18,764,765,776,780,784,823,831,836,860,864,868,878,889,894,898,907],{"__ignoreMap":244},[248,766,767,769,771,773],{"class":165,"line":250},[248,768,254],{"class":253},[248,770,258],{"class":257},[248,772,261],{"class":253},[248,774,775],{"class":257}," QgsExpression, qgsfunction\n",[248,777,778],{"class":165,"line":267},[248,779,271],{"emptyLinePlaceholder":270},[248,781,782],{"class":165,"line":274},[248,783,271],{"emptyLinePlaceholder":270},[248,785,786,788,790,792,794,796,798,800,802,804,806,808,810,813,816,818,821],{"class":165,"line":279},[248,787,33],{"class":282},[248,789,285],{"class":257},[248,791,289],{"class":288},[248,793,292],{"class":253},[248,795,296],{"class":295},[248,797,299],{"class":257},[248,799,302],{"class":288},[248,801,292],{"class":253},[248,803,307],{"class":295},[248,805,299],{"class":257},[248,807,312],{"class":288},[248,809,292],{"class":253},[248,811,812],{"class":257},"[], ",[248,814,815],{"class":288},"register",[248,817,292],{"class":253},[248,819,820],{"class":384},"False",[248,822,514],{"class":257},[248,824,825,827,829],{"class":165,"line":320},[248,826,323],{"class":253},[248,828,326],{"class":282},[248,830,329],{"class":257},[248,832,833],{"class":165,"line":332},[248,834,835],{"class":295},"    \"\"\"Return the UTM zone number containing a longitude.\"\"\"\n",[248,837,838,840,842,844,846,848,850,852,854,856,858],{"class":165,"line":338},[248,839,381],{"class":253},[248,841,385],{"class":384},[248,843,388],{"class":257},[248,845,391],{"class":253},[248,847,394],{"class":384},[248,849,397],{"class":257},[248,851,400],{"class":253},[248,853,403],{"class":384},[248,855,397],{"class":257},[248,857,391],{"class":253},[248,859,410],{"class":384},[248,861,862],{"class":165,"line":344},[248,863,271],{"emptyLinePlaceholder":270},[248,865,866],{"class":165,"line":349},[248,867,271],{"emptyLinePlaceholder":270},[248,869,870,873,876],{"class":165,"line":355},[248,871,872],{"class":253},"class",[248,874,875],{"class":282}," MyPlugin",[248,877,675],{"class":257},[248,879,880,883,886],{"class":165,"line":361},[248,881,882],{"class":253},"    def",[248,884,885],{"class":282}," initGui",[248,887,888],{"class":257},"(self):\n",[248,890,891],{"class":165,"line":367},[248,892,893],{"class":257},"        QgsExpression.registerFunction(utm_zone)\n",[248,895,896],{"class":165,"line":373},[248,897,271],{"emptyLinePlaceholder":270},[248,899,900,902,905],{"class":165,"line":378},[248,901,882],{"class":253},[248,903,904],{"class":282}," unload",[248,906,888],{"class":257},[248,908,910,913,916],{"class":165,"line":909},15,[248,911,912],{"class":257},"        QgsExpression.unregisterFunction(",[248,914,915],{"class":295},"\"utm_zone\"",[248,917,514],{"class":257},[14,919,920,416,922,925,926,434,929,932,933,935,936,940,941,945],{},[213,921,415],{},[18,923,924],{},"register=False"," builds the function object without adding it to the registry, so the decorator becomes a description rather than a side effect at import time. ",[18,927,928],{},"registerFunction()",[18,930,931],{},"unregisterFunction()"," then bracket the plugin's lifetime exactly. ",[18,934,931],{}," takes the ",[937,938,939],"em",{},"name"," as a string, not the object. This pairing is what makes the plugin reloadable — see ",[26,942,944],{"href":943},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","Reload a QGIS Plugin Without Restarting"," for why that matters during development.",[14,947,948,949,952,953,956],{},"The registry is process-wide and names must be unique. Registering a name that already exists fails, which is precisely the error a developer hits on the second reload after forgetting to unregister. Prefixing your functions — ",[18,950,951],{},"acme_utm_zone"," rather than ",[18,954,955],{},"utm_zone"," — also avoids colliding with someone else's plugin or a future built-in.",[202,958,960],{"id":959},"use-it-and-confirm-it-is-available","Use it and confirm it is available",[14,962,963],{},"Once registered, the function is ordinary. It can be evaluated from Python, typed into the field calculator, or used in a data-defined property.",[14,965,966],{},[38,967,970,973,976,979,986,989,995,1015,1024,1037,1048,1054,1058],{"viewBox":968,"role":41,"ariaLabel":969,"xmlns":43},"0 0 760 214","A registration lifecycle timeline: import defines the function, initGui registers it, the session uses it, and unload unregisters it, with a warning that a missing unload step breaks the next reload",[45,971,972],{},"The registration lifecycle across a plugin reload",[49,974,975],{},"A horizontal timeline with four moments. At import the decorated function is defined but not registered. At initGui registerFunction adds it to the registry. During the session it is callable from expressions. At unload unregisterFunction removes it. A branch below shows that skipping unload causes the next initGui to fail with a duplicate name error.",[53,977],{"x":55,"y":55,"width":56,"height":978,"fill":58},"214",[60,980,981],{},[63,982,984],{"id":983,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"fnLifeArrow",[72,985],{"d":74,"fill":93},[77,987,988],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Four moments — and one of them is easy to skip",[165,990],{"x1":991,"y1":992,"x2":993,"y2":992,"stroke":93,"style":994},"40","94","720","stroke-width:2.5;marker-end:url(#fnLifeArrow)",[996,997,998,1004,1008,1012],"g",{},[999,1000],"circle",{"cx":1001,"cy":992,"r":1002,"fill":1003},"96","9","#59645f",[77,1005,261],{"x":1001,"y":1006,"style":1007,"fill":82,"textAnchor":83},"70","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif",[77,1009,1011],{"x":1001,"y":1010,"style":199,"fill":75,"textAnchor":83},"122","defined,",[77,1013,924],{"x":1001,"y":1014,"style":199,"fill":75,"textAnchor":83},"138",[996,1016,1017,1020,1022],{},[999,1018],{"cx":1019,"cy":992,"r":1002,"fill":136},"272",[77,1021,758],{"x":1019,"y":1006,"style":1007,"fill":136,"textAnchor":83},[77,1023,928],{"x":1019,"y":1010,"style":199,"fill":75,"textAnchor":83},[996,1025,1026,1028,1031,1034],{},[999,1027],{"cx":173,"cy":992,"r":1002,"fill":117},[77,1029,1030],{"x":173,"y":1006,"style":1007,"fill":117,"textAnchor":83},"session",[77,1032,1033],{"x":173,"y":1010,"style":199,"fill":75,"textAnchor":83},"callable in every",[77,1035,1036],{"x":173,"y":1014,"style":199,"fill":75,"textAnchor":83},"expression field",[996,1038,1039,1043,1046],{},[999,1040],{"cx":1041,"cy":992,"r":1002,"fill":1042},"640","#b45309",[77,1044,1045],{"x":1041,"y":1006,"style":1007,"fill":1042,"textAnchor":83},"unload()",[77,1047,931],{"x":1041,"y":1010,"style":199,"fill":75,"textAnchor":83},[53,1049],{"x":1050,"y":1051,"width":1052,"height":1053,"rx":67,"fill":116,"stroke":193,"style":137},"332","158","396","42",[77,1055,1057],{"x":1056,"y":89,"style":104,"fill":75,"textAnchor":83},"530","skip it and the next initGui() fails: name already registered",[165,1059],{"x1":1041,"y1":1060,"x2":1061,"y2":1062,"stroke":193,"style":1063},"104","560","154","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#fnLifeArrow)",[239,1065,1067],{"className":241,"code":1066,"language":243,"meta":244,"style":244},"from qgis.core import QgsExpression, QgsExpressionContext\n\nprint(\"utm_zone\" in [f.name() for f in QgsExpression.Functions()])\n\ncontext = QgsExpressionContext()\nprint(QgsExpression(\"utm_zone(12.49)\").evaluate(context))   # 33\n",[18,1068,1069,1080,1084,1111,1115,1125],{"__ignoreMap":244},[248,1070,1071,1073,1075,1077],{"class":165,"line":250},[248,1072,254],{"class":253},[248,1074,258],{"class":257},[248,1076,261],{"class":253},[248,1078,1079],{"class":257}," QgsExpression, QgsExpressionContext\n",[248,1081,1082],{"class":165,"line":267},[248,1083,271],{"emptyLinePlaceholder":270},[248,1085,1086,1089,1091,1093,1096,1099,1102,1105,1108],{"class":165,"line":274},[248,1087,1088],{"class":384},"print",[248,1090,285],{"class":257},[248,1092,915],{"class":295},[248,1094,1095],{"class":253}," in",[248,1097,1098],{"class":257}," [f.name() ",[248,1100,1101],{"class":253},"for",[248,1103,1104],{"class":257}," f ",[248,1106,1107],{"class":253},"in",[248,1109,1110],{"class":257}," QgsExpression.Functions()])\n",[248,1112,1113],{"class":165,"line":279},[248,1114,271],{"emptyLinePlaceholder":270},[248,1116,1117,1120,1122],{"class":165,"line":320},[248,1118,1119],{"class":257},"context ",[248,1121,292],{"class":253},[248,1123,1124],{"class":257}," QgsExpressionContext()\n",[248,1126,1127,1129,1132,1135,1138],{"class":165,"line":332},[248,1128,1088],{"class":384},[248,1130,1131],{"class":257},"(QgsExpression(",[248,1133,1134],{"class":295},"\"utm_zone(12.49)\"",[248,1136,1137],{"class":257},").evaluate(context))   ",[248,1139,1141],{"class":1140},"sjoCn","# 33\n",[14,1143,1144,416,1146,1149,1150,1153],{},[213,1145,415],{},[18,1147,1148],{},"QgsExpression.Functions()"," returns every function the engine knows about, which is the definitive check that registration succeeded — far more reliable than opening the dialog and scrolling. Evaluating with a bare context works here because ",[18,1151,1152],{},"utm_zone()"," references neither fields nor variables.",[202,1155,1157],{"id":1156},"deciding-whether-a-custom-function-is-the-right-answer","Deciding whether a custom function is the right answer",[14,1159,1160],{},"A custom function is powerful and slightly costly: it makes every project that uses it depend on your plugin. Three cheaper alternatives cover a surprising share of the cases people reach for it.",[14,1162,1163],{},[38,1164,1167,1170,1173,1176,1183,1186,1190,1196,1199,1203,1206,1210,1213,1217,1220,1223,1226,1229,1232,1236,1239,1242,1245,1248,1252,1256,1259,1262,1265,1268,1272,1277],{"viewBox":1165,"role":41,"ariaLabel":1166,"xmlns":43},"0 0 760 268","Four options ordered by portability: a built-in expression, a project variable, a stored expression preset, and a custom Python function, with the trade-off between portability and power",[45,1168,1169],{},"Four ways to reuse expression logic",[49,1171,1172],{},"Four options are laid out from most portable to most powerful. A built-in expression works everywhere with no dependency. A project variable stores a value inside the project file. A stored expression preset travels with the user profile. A custom Python function is the most capable but requires the plugin to be installed wherever the project is opened.",[53,1174],{"x":55,"y":55,"width":56,"height":1175,"fill":58},"268",[60,1177,1178],{},[63,1179,1181],{"id":1180,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"fnPickArrow",[72,1182],{"d":74,"fill":1003},[77,1184,1185],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Reach for a custom function last, not first",[53,1187],{"x":87,"y":1188,"width":1189,"height":187,"rx":91,"fill":116,"stroke":136,"style":94},"52","172",[77,1191,1195],{"x":1192,"y":1193,"style":1194,"fill":136,"textAnchor":83},"102","78","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","built-in expression",[77,1197,1198],{"x":1192,"y":1060,"style":199,"fill":75,"textAnchor":83},"CASE, coalesce,",[77,1200,1202],{"x":1192,"y":1201,"style":199,"fill":75,"textAnchor":83},"120","scale_exp, aggregates",[77,1204,1205],{"x":1192,"y":108,"style":199,"fill":136,"textAnchor":83},"no dependency",[77,1207,1209],{"x":1192,"y":1208,"style":199,"fill":75,"textAnchor":83},"170","opens anywhere",[53,1211],{"x":1212,"y":1188,"width":1189,"height":187,"rx":91,"fill":116,"stroke":93,"style":94},"204",[77,1214,1216],{"x":1215,"y":1193,"style":1194,"fill":93,"textAnchor":83},"290","project variable",[77,1218,1219],{"x":1215,"y":1060,"style":199,"fill":75,"textAnchor":83},"@survey_year",[77,1221,1222],{"x":1215,"y":1201,"style":199,"fill":75,"textAnchor":83},"set once, used often",[77,1224,1225],{"x":1215,"y":108,"style":199,"fill":93,"textAnchor":83},"stored in the project",[77,1227,1228],{"x":1215,"y":1208,"style":199,"fill":75,"textAnchor":83},"travels with the file",[53,1230],{"x":1231,"y":1188,"width":1189,"height":187,"rx":91,"fill":116,"stroke":117,"style":94},"392",[77,1233,1235],{"x":1234,"y":1193,"style":1194,"fill":117,"textAnchor":83},"478","stored expression",[77,1237,1238],{"x":1234,"y":1060,"style":199,"fill":75,"textAnchor":83},"saved preset in the",[77,1240,1241],{"x":1234,"y":1201,"style":199,"fill":75,"textAnchor":83},"expression builder",[77,1243,1244],{"x":1234,"y":108,"style":199,"fill":117,"textAnchor":83},"per user profile",[77,1246,1247],{"x":1234,"y":1208,"style":199,"fill":75,"textAnchor":83},"not shared automatically",[53,1249],{"x":1250,"y":1188,"width":1251,"height":187,"rx":91,"fill":116,"stroke":1042,"style":94},"580","164",[77,1253,1255],{"x":1254,"y":1193,"style":1194,"fill":1042,"textAnchor":83},"662","custom function",[77,1257,1258],{"x":1254,"y":1060,"style":199,"fill":75,"textAnchor":83},"arbitrary Python,",[77,1260,1261],{"x":1254,"y":1201,"style":199,"fill":75,"textAnchor":83},"external libraries",[77,1263,1264],{"x":1254,"y":108,"style":199,"fill":1042,"textAnchor":83},"needs the plugin",[77,1266,1267],{"x":1254,"y":1208,"style":199,"fill":75,"textAnchor":83},"installed everywhere",[165,1269],{"x1":991,"y1":1270,"x2":993,"y2":1270,"stroke":1003,"style":1271},"222","stroke-width:2;marker-end:url(#fnPickArrow)",[77,1273,1276],{"x":991,"y":1274,"style":1275,"fill":136},"246","font-size:11.5px;font-family:sans-serif","most portable",[77,1278,1281],{"x":993,"y":1274,"style":1279,"fill":1042,"textAnchor":1280},"text-anchor:end;font-size:11.5px;font-family:sans-serif","end","most powerful",[14,1283,1284],{},"A project variable is often all that is needed when the \"function\" is really a constant that changes per project:",[239,1286,1288],{"className":241,"code":1287,"language":243,"meta":244,"style":244},"from qgis.core import QgsExpressionContextUtils, QgsProject\n\nQgsExpressionContextUtils.setProjectVariable(\n    QgsProject.instance(), \"survey_year\", 2026\n)\n# now usable anywhere as @survey_year\n",[18,1289,1290,1301,1305,1310,1323,1327],{"__ignoreMap":244},[248,1291,1292,1294,1296,1298],{"class":165,"line":250},[248,1293,254],{"class":253},[248,1295,258],{"class":257},[248,1297,261],{"class":253},[248,1299,1300],{"class":257}," QgsExpressionContextUtils, QgsProject\n",[248,1302,1303],{"class":165,"line":267},[248,1304,271],{"emptyLinePlaceholder":270},[248,1306,1307],{"class":165,"line":274},[248,1308,1309],{"class":257},"QgsExpressionContextUtils.setProjectVariable(\n",[248,1311,1312,1315,1318,1320],{"class":165,"line":279},[248,1313,1314],{"class":257},"    QgsProject.instance(), ",[248,1316,1317],{"class":295},"\"survey_year\"",[248,1319,299],{"class":257},[248,1321,1322],{"class":384},"2026\n",[248,1324,1325],{"class":165,"line":320},[248,1326,514],{"class":257},[248,1328,1329],{"class":165,"line":332},[248,1330,1331],{"class":1140},"# now usable anywhere as @survey_year\n",[14,1333,1334,416,1336,1339,1340,1343,1344,1346],{},[213,1335,415],{},[18,1337,1338],{},"setProjectVariable()"," writes into the project file, so the variable travels with the ",[18,1341,1342],{},".qgz"," and needs no plugin at all. Any label, filter or data-defined property can then reference ",[18,1345,1219],{},". Reserve a custom function for logic that genuinely cannot be written as an expression — an external lookup, a library call, or a computation with real branching.",[202,1348,1350],{"id":1349},"qgis-version-compatibility","QGIS version compatibility",[14,1352,1353,1354,1356],{},"The examples target ",[213,1355,215],{}," (Python 3.12).",[1358,1359,1360,1376],"table",{},[1361,1362,1363],"thead",{},[1364,1365,1366,1370,1373],"tr",{},[1367,1368,1369],"th",{},"QGIS version",[1367,1371,1372],{},"Python",[1367,1374,1375],{},"Notes",[1377,1378,1379,1396,1407],"tbody",{},[1364,1380,1381,1385,1388],{},[1382,1383,1384],"td",{},"3.28 LTR",[1382,1386,1387],{},"3.9",[1382,1389,1390,1391,434,1393,1395],{},"Identical API. ",[18,1392,33],{},[18,1394,924],{}," both available.",[1364,1397,1398,1401,1404],{},[1382,1399,1400],{},"3.34 LTR",[1382,1402,1403],{},"3.12",[1382,1405,1406],{},"Baseline for this page.",[1364,1408,1409,1412,1414],{},[1382,1410,1411],{},"3.40 \u002F 3.44",[1382,1413,1403],{},[1382,1415,1416,434,1419,1422],{},[18,1417,1418],{},"usesGeometry",[18,1420,1421],{},"handlesNull"," decorator arguments gained clearer defaults; behaviour unchanged for the patterns here.",[14,1424,1425],{},"The decorator's signature has been stable throughout 3.x. Functions registered by one plugin are visible to all, so name collisions are a cross-plugin concern rather than a version one.",[202,1427,1429],{"id":1428},"troubleshooting","Troubleshooting",[207,1431,1432,1450,1462,1482,1491,1502],{},[210,1433,1434,1439,1440,1443,1444,1446,1447,1449],{},[213,1435,1436,224],{},[18,1437,1438],{},"Function is already registered"," A previous instance never unregistered. Call ",[18,1441,1442],{},"QgsExpression.unregisterFunction(\"name\")"," in ",[18,1445,1045],{},", and use ",[18,1448,924],{}," on the decorator so registration only happens where you control it.",[210,1451,1452,1458,1459,1461],{},[213,1453,1454,1455,1457],{},"The function returns ",[18,1456,553],{}," in labels but works in the field calculator."," A column it reads is missing from ",[18,1460,312],{},". The calculator fetches all attributes; the labelling engine fetches only the declared ones.",[210,1463,1464,1470,1471,434,1473,1475,1476,1478,1479,1481],{},[213,1465,1466,1469],{},[18,1467,1468],{},"TypeError"," about argument count."," The trailing ",[18,1472,433],{},[18,1474,437],{}," parameters are missing, or ",[18,1477,289],{}," was set to a number that does not match the signature. Use ",[18,1480,419],{}," and let QGIS count.",[210,1483,1484,1487,1488,1490],{},[213,1485,1486],{},"The function does not appear in the builder dialog."," It registered into a group that is collapsed, or it never registered at all. Check with ",[18,1489,1148],{}," rather than by eye.",[210,1492,1493,1496,1497,1499,1500,759],{},[213,1494,1495],{},"Errors are opaque."," A raised Python exception loses the feature context. Use ",[18,1498,541],{}," and return ",[18,1501,549],{},[210,1503,1504,1507],{},[213,1505,1506],{},"It works for you and not for a colleague."," The function only exists while the plugin providing it is enabled. A project whose labels depend on a custom function is not portable without that plugin.",[202,1509,1511],{"id":1510},"conclusion","Conclusion",[14,1513,1514,1516,1517,1519,1520,1522,1523,1443,1525,1527,1528,224],{},[18,1515,33],{}," turns a Python function into part of the expression language: reachable from the field calculator, labelling rules, filters and data-defined properties alike. The discipline that keeps it maintainable is small — declare ",[18,1518,312],{}," honestly, report errors through ",[18,1521,437],{},", register with ",[18,1524,924],{},[18,1526,758],{},", and always unregister in ",[18,1529,1045],{},[202,1531,1533],{"id":1532},"frequently-asked-questions","Frequently Asked Questions",[14,1535,1536,1539,1540,1542,1543,1545,1546,224],{},[213,1537,1538],{},"What are the feature and parent arguments for?","\nThey are supplied by the engine, not the caller. ",[18,1541,433],{}," is the feature being evaluated, letting a function read attributes that were not passed as arguments. ",[18,1544,437],{}," is the expression node, used to report an evaluation error through ",[18,1547,1548],{},"setEvalErrorString()",[14,1550,1551,1554,1555,1557,1558,1560,1561,224],{},[213,1552,1553],{},"Why does my function fail on the second plugin reload?","\nThe name is still registered from the first load. The expression function registry is process-wide, so every ",[18,1556,928],{}," needs a matching ",[18,1559,931],{}," in the plugin's ",[18,1562,1045],{},[14,1564,1565,1568,1569,1571],{},[213,1566,1567],{},"Do I have to set referenced_columns?","\nYou should. It tells QGIS which fields to fetch. Leaving it unset makes QGIS conservatively load every column, and setting it incompletely causes fields to come back ",[18,1570,553],{}," in contexts that honour the declaration, such as labelling.",[14,1573,1574,1577],{},[213,1575,1576],{},"Can other people use a project that depends on my custom function?","\nOnly if they have the plugin that registers it installed and enabled. Without it, the expression fails to resolve the function name. For portable projects, keep the logic in built-in functions.",[202,1579,1581],{"id":1580},"related","Related",[207,1583,1584,1589,1595,1599],{},[210,1585,1586,1588],{},[26,1587,29],{"href":28}," — the guide this recipe belongs to",[210,1590,1591],{},[26,1592,1594],{"href":1593},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","Evaluate a QGIS Expression in PyQGIS",[210,1596,1597],{},[26,1598,223],{"href":222},[210,1600,1601],{},[26,1602,944],{"href":943},[1604,1605,1606],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":244,"searchDepth":267,"depth":267,"links":1608},[1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619],{"id":204,"depth":267,"text":205},{"id":233,"depth":267,"text":234},{"id":557,"depth":267,"text":558},{"id":751,"depth":267,"text":752},{"id":959,"depth":267,"text":960},{"id":1156,"depth":267,"text":1157},{"id":1349,"depth":267,"text":1350},{"id":1428,"depth":267,"text":1429},{"id":1510,"depth":267,"text":1511},{"id":1532,"depth":267,"text":1533},{"id":1580,"depth":267,"text":1581},"Add your own function to the QGIS expression engine with the qgsfunction decorator, control its arguments and help text, and unregister it cleanly on plugin unload.","md",{"slug":1623,"type":1624,"breadcrumb":1625,"datePublished":1626,"dateModified":1626},"register-custom-expression-function-pyqgis","article","Custom Expression Function","2026-08-01","\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fregister-custom-expression-function-pyqgis",{"title":5,"description":1620},"pyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fregister-custom-expression-function-pyqgis\u002Findex","S0hq8b2doJmytEYYDMQKrtohNWpZEyU_ETp8N3agMZ0",1785585990473]