Framework

Codegen

What Python constructs compile to JavaScript — the complete reference for browser-side code.

Codegen

When you write Component and Module subclasses in Python, sprag build compiles them to Ragot ESM JavaScript. This page documents exactly what Python constructs are supported, what they compile to, and what will raise an error.

The rule

If it's in a Component or Module subclass, it compiles to JS. Everything else runs as Python on the server. The compile boundary is at the class level — there's no ambiguity.

Statements

Supported everywhere (methods, on_start, on_stop, etc.)

PythonJavaScript
x = valuelet x = value (first use) or x = value (reassign)
a, b = pairconst [a, b] = pair
self.foo = valuethis.foo = value
x += 5x += 5
self.count += 1this.count += 1
return valuereturn value
if / elif / elseif / else if / else
for x in itemsfor (const x of items)
for i in range(n)for (let i = 0; i < n; i++) (optimized, no array)
while conditionwhile (condition)
try / except / finallytry / catch / finally
break / continuebreak / continue
pass(nothing)
match / caseswitch-like chain (Python 3.10+)

Restricted in render()

Component render() methods are more restricted — only these statements are allowed:

  • Variable assignments: x = expression
  • Attribute assignments: self.foo = value
  • if/elif/else blocks
  • Return statements: return ui.div(...)

No for, while, or try blocks. Use ui.For() for iteration. if/elif/else blocks and ternary expressions both work:

# If statements work — useful for conditional returns
def render(self, props=None):
    if self.state.get("loading"):
        return ui.div("Loading...")
    return ui.div("Content")

# Ternaries work too — terser for single-expression cases
def render(self, props=None):
    return ui.div("Loading...") if self.state.get("loading") else ui.div("Content")
# Wrong — for loop in render()
def render(self, props=None):
    items = []
    for item in self.state.get("items", []):
        items.append(ui.li(item["text"]))
    return ui.ul(items)

# Right — ui.For()
def render(self, props=None):
    return ui.ul(
        ui.For(
            self.state.get("items", []),
            key=lambda i: i["id"],
            render=lambda i: ui.li(i["text"]),
        )
    )

Expressions

Literals and collections

PythonJavaScript
Nonenull
True / Falsetrue / false
42, 3.1442, 3.14
"hello""hello"
f"count: {n}"` count: ${n} `
[1, 2, 3][1, 2, 3]
(1, 2, 3)[1, 2, 3] (tuples become arrays)
{"key": value}{"key": value}
{**a, **b}{...a, ...b}

Operators

PythonJavaScript
+, -, *, /, %+, -, *, /, %
`a \b` (dict merge){...a, ...b}
==, !====, !==
<, <=, >, >=<, <=, >, >=
is, is not===, !==
in, not in.includes()
and, or&&, `\\`
not x!x
-x, +x-x, +x
a if cond else bcond ? a : b

Comprehensions (single generator only)

[x * 2 for x in items if x > 0]      # list comp
{k: v for k, v in pairs}              # dict comp
{x for x in items}                     # set comp → new Set(...)

Multi-generator comprehensions are not supported.

Lambda

key=lambda item: item["id"]

Compiles to an arrow function: (item) => item["id"].

Walrus operator

if (count := self.state.get("count", 0)) > 10:
    self.set_state({"is_large": True})

Works in statements. Not supported inside comprehensions or lambdas.

Builtin functions

PythonJavaScript
len(x)(x).length
str(x)String(x)
int(x)Math.trunc(Number(x))
float(x)Number(x)
bool(x)Boolean(x)
abs(x)Math.abs(x)
min(...)Math.min(...)
max(...)Math.max(...)
round(x)Math.round(x)
print(...)console.log(...)
range(n)Materialized array (but for i in range(n) optimizes to C-style loop)
sum(items).reduce((a, b) => a + b, 0)

Name mapping

Python snake_case identifiers are converted to JavaScript camelCase:

self.set_state(...)       → this.setState(...)
self.on_socket(...)       → this.onSocket(...)
self.call_action(...)     → this.callAction(...)
self.add_cleanup(...)     → this.addCleanup(...)
self.adopt_component(...) → this.adoptComponent(...)

User-defined method names follow the same rule: on_counter_changeonCounterChange.

Special namespaces

imports.* — declared JS modules

Access third-party JavaScript declared in page(modules={...}):

# page.py
my_page = page(..., modules={"chart": "/vendor/chart.esm.js"})

# modules.py
class ChartModule(Module):
    def on_start(self):
        lib = imports.chart
        self.chart = lib.create(self.element)

Every alias used via imports.* must be declared in modules={} — missing aliases are a hard build error.

browser.* — global scope

Access globalThis.* for browser APIs:

class ScrollModule(Module):
    def on_start(self):
        self.width = browser.innerWidth
        browser.console.log("started")

dom.* — Ragot DOM helpers

dom.query(".selector")         # $(selector)
dom.query_all(".items")        # $$(selector)
dom.animate_in(el, "fade")     # animateIn(el, "fade")
dom.animate_out(el, "fade")    # animateOut(el, "fade")

Timer units

Python timer methods use seconds. The codegen automatically converts to milliseconds:

self.timeout(self.tick, 0.5)   # → this.timeout(() => this.tick(), 500)
self.interval(self.poll, 30)   # → this.interval(() => this.poll(), 30000)

Method references as callbacks

When you pass self.method as a callback, it's automatically bound:

self.timeout(self.tick, 1)     # → this.timeout(() => this.tick(), 1000)
self.on(el, "click", self.handle)  # → this.on(el, "click", (...args) => this.handle(...args))

__init__ (Module only)

Module __init__ can contain normal compilable statements. The main restriction is that self.state and self.screen are runtime-owned:

class MyModule(Module):
    def __init__(self, screen=None, state=None):
        super().__init__(screen=screen, state=state or {})
        self.draft = None
        self.config = {"mode": "compact"}
        if state and state.get("expanded"):
            self.open = True

super().__init__() calls are stripped. Direct assignment to self.state or self.screen raises an error.

env() — environment variables

Access SPRAG_PUBLIC_* environment variables inlined at build time:

api_url = env("SPRAG_PUBLIC_API_URL")
debug = env("SPRAG_PUBLIC_DEBUG", "false", cast=bool)
all_env = public_env()

What's NOT supported

These Python constructs will raise JSCodegenError at compile time:

ConstructAlternative
Type annotations: x: int = 5Use x = 5
with statementUse explicit try/finally
del xAssign None or use store.delete()
assert xUse if not x:
Nested function/class defsLift to module scope
for/else, while/elseRemove the else branch
yield / yield fromMaterialise eagerly
Set literals: {1, 2, 3}Use list or set comprehension
** (power), // (floor div)Use Math.pow(), Math.floor()
Bitwise ops (except `\`)Not supported
Multi-generator comprehensionsUse nested loops
isinstance() checksNot supported in browser code
Subscript assignment: self.state["k"] = vUse self.set_state({...}) or self.patch({...})
ui.For() / ui.Grid() / ui.LazyImage() outside render()Move the call directly into render() — helper methods aren't scanned for mount-point wiring

Every error includes the source file, class name, line number, and a suggestion.

Inspecting output

To see what your Python compiles to:

sprag inspect /counter --rebuild

This shows the generated JavaScript with comments mapping back to your Python source lines.