plxpython3: the Python dialect¶
plxpython3 lets you write PostgreSQL functions with Python syntax. At
CREATE FUNCTION time plx transpiles the body to plpgsql and stores the plpgsql
in pg_proc.prosrc. The function runs on the standard plpgsql interpreter.
The language name is plxpython3, so it does not collide with the native
PL/Python (plpython3u). plxpython3 is a trusted language, unlike plpython3u.
Setup¶
Function basics¶
Blocks are defined by indentation, as in Python. A compound-statement header ends
with : and its body is indented. A scalar function ends with an explicit
return.
Function arguments are referenced by name. Any SQL expression is valid, because expressions are passed through to plpgsql and SQL unchanged.
Local variables and types¶
Assignment creates a local. plx infers the type from the first value when it is
an integer, numeric, text, or boolean literal. Otherwise annotate the variable
with #:: type.
n = 42 # inferred integer
label = "count" # inferred text
amount #:: numeric
amount = principal * rate
The annotation text is emitted verbatim, so %TYPE and %ROWTYPE work:
A constant uses the const suffix on the annotation:
Control flow¶
if / elif / else¶
Loops¶
for ... in range(...) is an integer loop. range(n) is 0 to n-1, range(a, b)
is a to b-1, range(a, b, step) adds a step.
continue and break map to CONTINUE and EXIT. pass is a no-op.
Loop labels¶
Working with data¶
Iterating a query¶
The row variable is a record. Field access is row.col. For bind parameters or
non-literal SQL, pass extra arguments: query(sql, a).
Iterating an array¶
The loop variable must be annotated with its element type.
Fetching one row¶
fetch_one returns all-NULL on no row. fetch_one! raises on zero or more than
one row.
Running SQL¶
perform(f"UPDATE counters SET n = n + 1 WHERE id = {cid}")
execute("INSERT INTO t(msg) VALUES ($1)", note)
return_query("SELECT id FROM vip ORDER BY id") # set-returning function
Use bind arguments in execute for untrusted input.
Cursors¶
c = open_cursor("SELECT v FROM t ORDER BY v")
row = fetch_from(c)
while found():
total = total + row.v
row = fetch_from(c)
close_cursor(c)
move_cursor(c) and move_cursor(c, n) map to MOVE.
Diagnostics¶
Set-returning functions¶
CREATE FUNCTION squares(n int) RETURNS SETOF int LANGUAGE plxpython3 AS $$
for i in range(1, n + 1):
return_next(i * i)
return
$$;
Errors¶
Raising¶
The call form raise("notice", "message") emits a RAISE at the named level
(notice, warning, info, log, debug, exception). A bare raise inside
an except handler re-raises.
Handling¶
try:
execute("INSERT INTO uniq(id) VALUES ($1)", k)
except PG::UniqueViolation as e:
raise("notice", f"dup on {e.constraint}")
finally:
perform("INSERT INTO log(msg) VALUES ('done')")
except Exception as e maps to WHEN OTHERS. To catch a specific condition,
name it with the PG:: spelling (except PG::UniqueViolation as e), which maps
to unique_violation. Accessors: e.message to SQLERRM, e.sqlstate to
SQLSTATE, and e.detail, e.hint, e.constraint, e.column, e.table,
e.schema, e.datatype to the matching GET STACKED DIAGNOSTICS fields.
Assertions¶
Expressions¶
- Interpolation: f-strings,
f"total is {amount}". Plain strings do not interpolate.{{and}}are literal braces. - Comparison:
==and!=map to=and<>.x is Nonebecomesx IS NULLandx is not Nonebecomesx IS NOT NULL. - Boolean:
and,or,notmap toAND,OR,NOT. - Ternary: Python's
a if c else bis not supported; use an explicitif. NonebecomesNULL.
Building strings in a loop¶
Concatenating onto a string in a loop is slow in plpgsql: s := s || 'x' is
O(n^2) because text is immutable and each step copies the whole string. Use +=
on a string variable, which plx lowers to its string builder (plx_strbuild):
On PostgreSQL 18 this is amortized O(1) per append. On PostgreSQL 13 to 17 it is
correct but not accelerated (the in-place optimization needs a PostgreSQL 18
feature). Note that += is treated as string append only when the variable is a
string; on a numeric variable it is numeric addition.
Trigger functions¶
A function returning trigger can be used as a trigger. Assign to NEW fields
and return NEW:
CREATE FUNCTION stamp() RETURNS trigger LANGUAGE plxpython3 AS $$
NEW.tag = f"row {NEW.id}"
return NEW
$$;
NEW, OLD, and the TG_ variables are available. Assigning to a record field
(NEW.col = e) or an array element is supported.
Semantic differences¶
These are intentional. plx pins semantics to SQL and plpgsql.
- Decimal literals infer
numeric, not a floating-point type. - Comparisons use SQL three-valued logic:
==and!=map to=and<>, so a comparison involving NULL is unknown. Useis None/is not None(which becomeIS NULL/IS NOT NULL) to test for NULL. A positiveif/whilecondition treats NULL as false. - Integer division follows SQL rules, not Python's
/(float) and//(floor). Division and modulo truncate toward zero:-7 // 2is-4in Python but the SQL result here is-3, and-7 % 2is1in Python but-1here. - Interpolating a NULL in an f-string renders as an empty string, not the Python
string
"None"; the whole string is never made NULL. - Comparisons use SQL type resolution.
1 == "1"compares an integer to a string literal, which SQL coerces and treats as equal; in Python it is false. - Python truthiness is not emulated. A condition must be a boolean expression. A non-boolean condition is an error reported by plpgsql when the function runs.
- Locals are function-scoped, matching Python function scope.
Not supported¶
Rejected at CREATE FUNCTION time with a line number:
def,class,lambda, decorators, comprehensions, generators.import, modules, and the Python standard library.- Tuples, lists, dicts, and sets as general values.
match/case.- The conditional expression
a if c else b. - Per-block local scope (locals are function-scoped by design).
See PARITY.md for the full plpgsql construct matrix and ARCHITECTURE.md for how plx maps to the plpgsql engine.