The variable system turns documents into reusable templates. Variables are typed, can have defaults, and are substituted into text via {name} placeholders or bound directly to images and QR codes.
doc.define_variable(
name="recipient",
type="text",
default="",
required=True,
description="Person being awarded the certificate",
choices=None, # optional list of allowed values
)
| Type constant | String | Purpose |
|---|---|---|
VAR_TEXT |
"text" |
String value |
VAR_NUMBER |
"number" |
Integer or float |
VAR_DATE |
"date" |
Date string (ISO 8601: YYYY-MM-DD) |
VAR_BOOL |
"bool" |
True / False |
VAR_URL |
"url" |
URL string (validated as a URL on set) |
VAR_IMAGE |
"image" |
Resource ID or file path |
VAR_QR |
"qr" |
Text/URL to encode as QR (used by QRCode objects) |
You can use the constants or the strings interchangeably:
from edof import VAR_NUMBER
doc.define_variable("score", type=VAR_NUMBER, default=0)
doc.define_variable("score", type="number", default=0) # equivalent
required: bool — if True, doc.validate() fails when the variable has no valuedefault — value used when no explicit value is setlabel: str — display label for editor / CLI promptshelp: str — descriptive textchoices: list — restrict text values to this list (becomes a dropdown in the editor)max_length: int — for text variables; trims at this lengthdoc.set_variable("recipient", "Jan Novák")
doc.set_variable("score", 95)
# Bulk
doc.fill_variables({
"recipient": "Jan Novák",
"score": 95,
"date": "2026-05-04",
})
fill_variables() is a wrapper that calls set_variable() repeatedly.
Type checking is enforced — setting a non-numeric value on a number variable raises EdofVariableError. Same for invalid dates, malformed URLs, etc.
The {variable_name} syntax substitutes at render time:
doc.define_variable("name")
doc.define_variable("amount", type="number")
page.add_textbox(15, 15, 180, 8, "Hello {name}, you owe {amount} CZK.")
doc.set_variable("name", "Alice")
doc.set_variable("amount", 1500)
doc.export_pdf("invoice.pdf")
The result reads “Hello Alice, you owe 1500 CZK.”
By default, numbers render as Python’s str(value). To format more carefully (currency, decimals, etc.), pre-format the string in code before setting the variable:
doc.set_variable("amount", f"{1500.50:,.2f}") # "1,500.50"
Or define the variable as text instead of number.
Dates render in ISO format by default. For custom formatting, do it in code:
import datetime
date = datetime.date.today()
doc.set_variable("today", date.strftime("%d. %B %Y")) # "4. May 2026"
Some object types support a variable attribute that overrides their content at render time.
doc.define_variable("title", default="Untitled")
tb = page.add_textbox(15, 15, 180, 12, "")
tb.variable = "title"
# tb.text is ignored at render — value of "title" is used instead
This is equivalent to tb.text = "{title}", but the binding is explicit and the editor shows it differently in the UI.
doc.define_variable("logo", type="image")
ib = page.add_image(default_logo_id, x=15, y=15, w=40, h=40)
ib.variable = "logo"
# Now at render time, "logo" can be a resource ID OR a path to a file:
doc.set_variable("logo", "/path/to/customer_logo.png")
doc.export_pdf("output.pdf")
# Or another resource ID already in the document:
new_id = doc.add_resource_from_file("alternate.png")
doc.set_variable("logo", new_id)
doc.define_variable("verify_url", type="url")
qr = page.add_qrcode(160, 15, 30, 30, data="https://default.com")
qr.variable = "verify_url"
doc.set_variable("verify_url", "https://verify.example.com/abc123")
doc.variables is a VariableStore instance. Most users don’t interact with it directly, but it has these methods:
store = doc.variables
store.names() # list of all variable names
store.get_def("score") is not None # defined?
store.get("score") # current value (or default if not set)
store.get_def("score") # VariableDef object
store.set("score", 42) # same as doc.set_variable
store.reset_all() # drop values (fall back to defaults)
store.all_values() # dict of all current values
store.undefine("score") # remove the definition entirely
store.missing_required() # names of required vars without a value
The metadata about a variable (separate from its current value):
defn = doc.variables.get_def("score")
print(defn.name, defn.type, defn.default, defn.required)
Fields: name, type, default, required, label, help, choices, max_length.
page.repeat_objects() is the powerful template feature: it duplicates a set of “template” objects for each row of a data list and auto-paginates onto new pages.
# Build a header that goes on every page
header = page.add_textbox(15, 10, 180, 8, "Sales Report")
header.style.bold = True
# Build a template row
row_tpl = page.add_textbox(15, 25, 180, 8, "{name}: {amount} CZK")
row_tpl.style.font_size = 10
# Important: remove the template from the page before repeating it
page.objects.remove(row_tpl)
# Generate one row per data entry; repeat_objects creates new pages as needed
new_pages = page.repeat_objects(
template=[row_tpl],
data=[
{"name": "Alice", "amount": 1500},
{"name": "Bob", "amount": 2300},
{"name": "Carol", "amount": 1850},
# ... 200 more ...
],
gap=2.0, # vertical spacing in mm between repeated rows
)
print(f"Added {len(new_pages)} extra pages.")
template: list[EdofObject] — the objects to repeat. They can use {column_name} placeholders that match keys of the data dicts.data: list[dict] — list of records. Each record’s keys are the placeholders.gap: float — vertical space in mm between repetitions (default: 2.0)data:
{column} placeholders are replaced with row[column]header = page.add_textbox(15, 0, 100, 6, "{name}")
header.style.bold = True
header.style.font_size = 10
body = page.add_textbox(15, 6, 180, 12, "{description}")
body.style.font_size = 8
shape = page.add_shape("line", 15, 18, 180, 0)
shape.points = [[0, 0], [180, 0]]
# Remove the template objects from the page
page.objects.remove(header)
page.objects.remove(body)
page.objects.remove(shape)
# Repeat them as a unit
page.repeat_objects(
template=[header, body, shape],
data=[
{"name": "Section 1", "description": "Some description text."},
{"name": "Section 2", "description": "More description."},
# ...
],
gap=4.0,
)
new_pages = page.repeat_objects(template=[row_tpl], data=data, gap=1.0)
# Add a page number to every page
for i, p in enumerate([page] + new_pages):
pn = p.add_textbox(95, 285, 20, 6, f"Page {i+1}")
pn.style.font_size = 8
pn.style.alignment = "center"
doc.validate() checks (among other things) that all required=True variables have values:
doc.define_variable("recipient", required=True)
issues = doc.validate()
print(issues)
# ['Required variable "recipient" has no value']
doc.set_variable("recipient", "Alice")
issues = doc.validate()
print(issues)
# []
fill_variables() does NOT auto-validate; call validate() explicitly to check.
visible_ifobj.visible_if is a small expression evaluated against doc.variables at render time:
discount_label = page.add_textbox(15, 200, 180, 8, "DISCOUNT: -{discount} CZK")
discount_label.visible_if = "discount > 0"
vip_section = page.add_group()
vip_section.visible_if = "tier == 'gold' or score >= 90"
See reference/02-objects.md for the full expression syntax.
Besides document-level {name} variables, any span of text can become a
batch variable: the span gets a stable rid on its runs plus a human
var_name, and a batch column bound to that rid fills it per row. The
rid survives editing around the span and page reflow.
Naming (v4.4.0). Every variable/column name is unique, always. An
unnamed column gets a systematic name persisted from the object and
attribute (“textbox-1.text”, “shape-1.fill-color”; an object’s own name
from the Objects panel wins over the type prefix). A custom name that is
already taken is auto-suffixed (“Name_2”) at creation; renaming to a taken
name is rejected in the table header editor and auto-suffixed in the
variable rename flows. Uniqueness is case-insensitive and enforced at the
model level (BatchConfig.add_column, unique_header, header_in_use),
so the [{Header}] filename tags and CSV header matching are never
ambiguous.
Creating: select text in the editor and use the { } toolbar button,
Ctrl+Shift+B, or the right-click menu. The dialog also offers Add to an
existing variable (the selection becomes another occurrence of it).
Managing (Objects panel). Every variable appears as a virtual child of its text box (rainbow chip). A plain click focuses and highlights the span; the checkbox on the row selects it into the shared set — check several to edit shared run attributes or to link them. Right-click offers Rename, Remove, Change range (use editor selection) — the current editor selection becomes the variable’s new span while its columns and values survive — and the shared attribute toggles.
Linking variables (v4.4.0). Link objects to variable… can LINK other
variables to a column: one value then drives every linked span, but each
variable keeps its own rid, name and panel entry (BatchColumn.
extra_run_ids). Unchecking unlinks. Redundant same-attribute columns of
newly linked variables are dropped so two columns never fight over a span.
Highlighting. View → Show Variables marks every variable span with a rainbow underlay; selecting a batch-table column highlights exactly the span that column drives. Both are view-only aids and never export.
Header/footer. Variables work inside the header/footer band; the rid is persisted to the body template so it survives repagination (see the Header & Footer reference).
Typing at a span boundary never extends the span: new text typed right after a variable (or anchor/link) is plain — the identity does not leak into what you type next; typing inside the span still belongs to it.