Registering a Custom Expression Function with qgsfunction
Add your own function to the QGIS expression engine with the qgsfunction decorator: the feature and parent arguments, declaring referenced columns, reporting…
TL;DR: decorate a Python function with @qgsfunction, register it when your plugin loads, and it becomes available in every place QGIS evaluates expressions — the field calculator, symbology, labelling, Processing parameters and your own code. This page is part of the attribute data and the expression engine guide.
Complete Runnable Code
"""A custom expression function, with registration and teardown."""
from qgis.core import QgsExpression, qgsfunction
@qgsfunction(args="auto", group="Planning", referenced_columns=[])
def density_band(density, feature, parent):
"""Return the planning density band for a value in dwellings per hectare.
<h4>Syntax</h4>
<code>density_band(density)</code>
<h4>Example</h4>
<code>density_band("dwellings" / "area_ha") → 'medium'</code>
"""
if density is None:
return None
if density < 20:
return "low"
return "medium" if density < 60 else "high"
def register() -> None:
"""Register the function. Safe to call more than once."""
QgsExpression.registerFunction(density_band)
def unregister() -> None:
"""Remove the function. Call this from the plugin's unload()."""
QgsExpression.unregisterFunction("density_band")
The docstring is not decoration: QGIS renders it as the function’s help text in the expression builder, HTML and all. A function with no docstring appears in the list with an empty help panel, which is how users decide it is not meant for them.
Architecture Breakdown
The three trailing arguments
Every custom function receives feature and parent after its declared arguments, and with args="auto" the decorator works out the arity from the signature. feature is the feature being evaluated — useful when the function needs more than its arguments — and parent is the expression node, which is how a function reports an error:
@qgsfunction(args="auto", group="Planning", referenced_columns=[])
def safe_ratio(numerator, denominator, feature, parent):
"""Divide, reporting a clear expression error instead of raising."""
if denominator in (None, 0):
parent.setEvalErrorString("denominator is zero or NULL")
return None
return numerator / denominator
Calling setEvalErrorString() is what surfaces the problem as an expression error the user can read, rather than a Python traceback in the log that they will never see.
referenced_columns and usesGeometry
These two arguments tell the expression engine what the function needs, so QGIS can request exactly that and no more. Declaring referenced_columns=[] on a function that only uses its arguments means the engine does not have to fetch every attribute of every feature to evaluate it.
The defaults are conservative — QGIS assumes the function might need everything — so leaving them alone is correct but slow. On a rendering pass over a large layer, the difference is visible.
Registration and lifetime
registerFunction() adds the function to a process-wide registry that outlives your plugin. If unload() does not unregister it, the function survives a plugin reload and the second registration either fails or shadows the first, depending on version — which is why a plugin under development ends up with an expression function that no longer matches its source.
Wiring It Into a Plugin
from . import expressions # the module containing the decorated functions
class MyPlugin:
def __init__(self, iface):
self.iface = iface
def initGui(self) -> None:
expressions.register()
def unload(self) -> None:
expressions.unregister()
Registering in initGui() rather than at import time matters: importing your plugin package happens before the application is fully up, and a function registered then may be discarded when the expression registry is initialised.
What Belongs in an Expression Function
A registered function is a piece of public interface: it appears in a list users browse, it is called from expressions saved in project files, and removing it later breaks those projects. That argues for a small, deliberate set rather than exposing everything a plugin can do.
The functions that earn their place share a shape. They are pure — the same arguments always give the same answer — they are cheap enough to run once per feature during a repaint, and they encode a rule the organisation genuinely reuses across styling, labelling and analysis. A density banding, a reference-number formatter, a classification somebody would otherwise retype into twenty expressions: those belong.
What does not belong is anything with a side effect, anything that touches the network or the filesystem, and anything whose answer depends on state the expression engine cannot see. Such a function will appear to work in the field calculator and then behave unpredictably during rendering, where the evaluation order and the number of calls are both outside your control.
One further consideration is versioning. Because a function name is referenced from saved projects, changing its behaviour is a compatibility event even when the signature is unchanged — a map styled last year will quietly render differently. When the rule genuinely changes, adding a second function alongside the first is usually kinder than editing the original in place.
Production Best Practices
- Namespace the name. A function called
formatwill collide with somebody’s expectations. Prefix it with the plugin or the domain. - Return
Nonefor NULL input rather than raising. Expression functions are called on every row, including the ones with gaps. - Declare
referenced_columnsexplicitly, even when it is empty — especially when it is empty. - Write the docstring as help text, with a syntax line and an example. It is the only documentation most users will ever see.
- Unregister in
unload(), or reloading the plugin during development will leave a stale function behind. - Keep it fast. A function used in a symbology rule runs once per feature per repaint; a database lookup inside one will make the canvas unusable.
Frequently Asked Questions
Can a custom function query another layer?
It can, and it is usually a bad idea. The function runs per feature, potentially during rendering, and a lookup against another layer turns a repaint into thousands of queries. Where a cross-layer value is genuinely needed, materialise it into a column first — with a join or a Processing algorithm — and let the expression read the column.
Do custom functions work in a headless script?
Yes. Register the function after initQgis() and it is available to every expression evaluated in that process, including ones inside Processing algorithms. What does not carry across is any project the function implicitly depends on, so keep them pure functions of their arguments.
Will users see my function in the expression builder?
Yes, under whatever group you declared, with the docstring as its help. That visibility is the main reason to register a function rather than keeping it in Python: it turns a rule your code knows about into one the whole team can use in styling, labelling and the field calculator.
What happens to projects using my function if the plugin is uninstalled?
Every expression referencing it starts failing, because the name no longer resolves. Symbology falls back to drawing nothing for the affected rules, labels disappear, and the field calculator reports an unknown function.
That is a strong argument for treating the function name as a permanent commitment, and for prefixing it so it is obvious which plugin a project depends on. It is also a reason to prefer writing derived values into real columns when a project has to be shareable with people who do not have the plugin installed.
Can a function take a variable number of arguments?
Yes. Setting args=-1 gives the function a list of arguments instead of named parameters, which suits aggregate-style helpers where the caller decides how many values to pass. The cost is that the expression builder can no longer show a meaningful signature, and mistakes that would have been arity errors become runtime surprises.
For most rules a fixed signature with an optional trailing argument is clearer to the person writing the expression, and clearer is worth more here than flexible: the audience for a registered function includes people who will never read its source.
Can I test a custom function without QGIS running?
Not directly — the decorator produces a QGIS function object rather than a plain callable. The practical answer is to keep the logic in an ordinary Python function and make the decorated one a thin wrapper over it. The logic is then testable with nothing but pytest, and the wrapper contains only the argument unpacking and the error reporting, which is small enough to verify by reading.
Related
- Attribute Data and the Expression Engine — the parent guide covering scopes and contexts
- Layer Styling and Symbology Automation — where a registered function is most useful
- Plugin Lifecycle and Resource Management — registering in initGui and undoing it in unload