Skip to content

Live methods

Live methods let JavaScript call Python functions in real-time without page reloads. Use them for dynamic interfaces, live validation, background processing, and any interaction that needs immediate server response.

The @live decorator

Mark a page method with @live to make it callable from JavaScript:

class Counter(Page):
    @live
    def increment(page, player):
        player.counter += 1
        return player.counter

Live methods:

  • Receive page and player as the first two arguments
  • Can accept additional typed parameters
  • Can return data back to the caller
  • Can be sync or async

See the counter example

Calling from JavaScript

Use uproot.invoke() to call live methods:

uproot.invoke("increment").then(count => {
    document.getElementById("counter").innerText = count;
});

Pass arguments after the method name:

// Single argument
uproot.invoke("set_value", 42);

// Multiple arguments
uproot.invoke("save_response", "question1", "answer");

// With error handling
uproot.invoke("increment")
    .then(result => updateDisplay(result))
    .catch(error => showError("Operation failed"));

Request-response pattern

Return data directly from the live method. The JavaScript Promise resolves with the returned value:

class Quiz(Page):
    @live
    def check_answer(page, player, answer: str):
        correct = answer.lower() == "paris"
        if correct:
            player.score += 1
        return {"correct": correct, "score": player.score}
uproot.invoke("check_answer", userInput).then(result => {
    if (result.correct) {
        showSuccess("Correct! Score: " + result.score);
    } else {
        showError("Try again");
    }
});

Type validation

Live method parameters are validated using Python type hints:

@live
def place_bid(page, player, amount: float, item_id: int):
    # amount must be a float
    # item_id must be an integer
    player.bid = amount
    return {"placed": amount}

Invalid arguments raise an error that rejects the JavaScript Promise. Supported types include:

  • str, int, float, bool
  • list, dict
  • Optional[T] for nullable values
  • Custom types via Pydantic

Connection lifecycle

Handle WebSocket connection events in JavaScript:

uproot.onReady(() => {
    // WebSocket connected, safe to invoke methods
    uproot.invoke("get_initial_state").then(initDisplay);
});

uproot.onReconnect(() => {
    // Reconnected after disconnect, refresh state
    uproot.invoke("get_current_state").then(refreshDisplay);
});

uproot.onDisconnect(() => {
    // Connection lost, show warning
    showConnectionWarning();
});

Example: dynamic counter

A counter that updates without page reload:

Python

def new_player(player):
    player.counter = 0


class Counter(Page):
    @live
    def increment(page, player):
        player.counter += 1
        return player.counter

    @live
    def reset(page, player):
        player.counter = 0
        return player.counter

Template

{% extends "Base.html" %}

{% block main %}
<p>Count: <span id="count">{{ player.counter }}</span></p>
<button onclick="increment()">+1</button>
<button onclick="reset()">Reset</button>

<script>
function increment() {
    uproot.invoke("increment").then(count => {
        document.getElementById("count").innerText = count;
    });
}

function reset() {
    uproot.invoke("reset").then(count => {
        document.getElementById("count").innerText = count;
    });
}
</script>
{% endblock main %}

See the counter example ยท counter_alpine example (with Alpine.js)

Example: live form validation

Validate input as the user types:

class Registration(Page):
    fields = dict(
        username=StringField(label="Username"),
    )

    @live
    def check_username(page, player, username: str):
        # Check if username is available
        taken = username.lower() in ["admin", "root", "system"]
        return {"available": not taken}
document.getElementById("username").addEventListener("input", (e) => {
    uproot.invoke("check_username", e.target.value).then(result => {
        if (result.available) {
            showValid("Username available");
        } else {
            showError("Username taken");
        }
    });
});

Example: fetching data

Load data dynamically without page reload:

class Dashboard(Page):
    @live
    def get_stats(page, player):
        return {
            "total_responses": player.session.response_count,
            "average_score": player.session.average_score,
        }
uproot.onReady(() => {
    // Fetch stats when page loads
    uproot.invoke("get_stats").then(stats => {
        document.getElementById("responses").innerText = stats.total_responses;
        document.getElementById("average").innerText = stats.average_score;
    });
});

// Refresh stats periodically
setInterval(() => {
    uproot.invoke("get_stats").then(updateDashboard);
}, 5000);

Async operations

Live methods can be async for I/O operations. Keep reads and writes of player, player.session, and other uproot storage outside the part of the method that awaits whenever you can:

@live
async def process_data(page, player, data: dict):
    # Do slow external work first.
    result = await some_async_function(data)

    # Then update uproot state without another await in between.
    player.processed = result
    return {"status": "complete"}

Advanced concurrency note

Special care must be taken when you want to allow live methods to interleave with async. Specifically:

Live method calls from the same browser page are not a transaction queue. If an advanced @live method reaches an await, another live method call for the same participant may run before the first one resumes. This matters only for methods whose correctness depends on a check-then-act sequence across an await.

Avoid this pattern:

@live
async def submit_once(page, player, payload: dict):
    if player.submitted:
        raise ValueError("Already submitted")

    result = await external_service(payload)

    player.result = result
    player.submitted = True

Prefer doing awaited work before entering the state-sensitive section, or keep the state-sensitive section free of awaits:

@live
async def submit_once(page, player, payload: dict):
    result = await external_service(payload)

    if player.submitted:
        raise ValueError("Already submitted")

    player.result = result
    player.submitted = True

For most live methods, including the examples in this guide, this is not a concern because the method does not await while it is reading or updating uproot state.

Example: real-effort task

Live methods are ideal for real-effort tasks where participants interact repeatedly without page reloads:

class Sumhunt(Page):
    timeout = 120

    @live
    def get_matrix(page, player):
        if player.matrix is None:
            player.matrix = generate_matrix(...)
        return player.matrix

    @live
    def propose_solution(page, player, solution: list[int]):
        if sum(solution) == TARGET and all(x in player.matrix for x in solution):
            player.solutions += 1
            player.matrix = generate_matrix(...)  # New puzzle
            return True
        return False
// Fetch puzzle on page load
uproot.onReady(() => {
    uproot.invoke("get_matrix").then(updateDisplay);
});

// Submit answer and get next puzzle
uproot.invoke("propose_solution", selectedNumbers).then(correct => {
    if (correct) {
        uproot.invoke("get_matrix").then(updateDisplay);
    }
});

See the sumhunt example

Summary

Feature Purpose
@live decorator Make method callable from JavaScript
uproot.invoke("method", args) Call live method from JavaScript
Return value Data returned to JavaScript Promise
Type hints Automatic parameter validation
uproot.onReady(fn) Run when WebSocket connects
uproot.onReconnect(fn) Run after reconnection
uproot.onDisconnect(fn) Run when connection lost

For broadcasting updates to multiple participants, see Real-time interactions.