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=None,
required=True,
label="Recipient name", # human-readable label (used in editor / CLI)
help="Person being awarded the certificate",
choices=None, # for "text" type: list of allowed values
max_length=None, # for "text" type: maximum string length
)
| 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.exists("score") # bool
store.get("score") # current value (or default if not set)
store.get_definition("score") # VariableDef object
store.set("score", 42) # same as doc.set_variable
store.unset("score") # remove value (falls back to default)
store.values() # dict of all current values
The metadata about a variable (separate from its current value):
defn = doc.variables.get_definition("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.