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.
CREATE EXTENSION plx;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.
CREATE FUNCTION add(a int, b int) RETURNS int LANGUAGE plxpython3 AS $$
return a + b
$$;Function arguments are referenced by name. Any SQL expression is valid, because expressions are passed through to plpgsql and SQL unchanged.
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 * rateThe annotation text is emitted verbatim, so %TYPE and %ROWTYPE work:
e #:: emp%ROWTYPE
s #:: emp.sal%TYPEA constant uses the const suffix on the annotation:
pi = 3.14159 #:: numeric constif score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
else:
grade = "F"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.
for i in range(1, n + 1):
total = total + i
while n > 0:
n = n - 1continue and break map to CONTINUE and EXIT. pass is a no-op.
outer: for i in range(n):
for j in range(n):
if i + j >= limit:
break outerfor row in query(f"SELECT id, amount FROM orders WHERE grp = {g}"):
total = total + row.amountThe row variable is a record. Field access is row.col. For bind parameters or
non-literal SQL, pass extra arguments: query(sql, a).
v #:: int
for v in values:
total = total + vThe loop variable must be annotated with its element type.
u = fetch_one(f"SELECT id, name FROM users WHERE id = {uid}")
return u.namefetch_one returns all-NULL on no row. fetch_one! raises on zero or more than
one row.
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 functionUse bind arguments in execute for untrusted input.
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.
rc = row_count() # GET DIAGNOSTICS rc = ROW_COUNT
found() # the FOUND variableCREATE FUNCTION squares(n int) RETURNS SETOF int LANGUAGE plxpython3 AS $$
for i in range(1, n + 1):
return_next(i * i)
return
$$;raise ValueError(f"negative: {v}")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.
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.
assert n > 0, "must be positive"- 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.
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):
s = "" #:: text
for row in query("SELECT name FROM t ORDER BY id"):
s += row.name
s += ","
return sOn 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.
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.
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.
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.