Search documentation

Find pages, sections, and content across all docs.

WavedashDocs

Python

Run Python games in the browser with Pyodide — no build step, just an HTML shell.

View example project on GitHub Playtest the example project

You can ship a Python game to Wavedash by running CPython in the browser with Pyodide. There's no build step — Pyodide loads from a CDN, reads your .py file at runtime, and exposes the browser globals (document, window, and Wavedash) to Python code through the js module.

Setup

Create a web/ folder with an index.html that boots Pyodide and a main.py with your game.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>My Python Game</title>
    <style>
      html, body { margin: 0; height: 100%; background: #0a0a0a; }
      #gameCanvas { width: 100%; height: 100%; display: block; }
    </style>
  </head>
  <body>
    <canvas id="gameCanvas"></canvas>
    <script type="module">
      import { loadPyodide } from "https://cdn.jsdelivr.net/pyodide/v0.29.3/full/pyodide.mjs";

      const pyodide = await loadPyodide({
        indexURL: "https://cdn.jsdelivr.net/pyodide/v0.29.3/full/",
      });

      const code = await fetch("./main.py").then((r) => r.text());
      await pyodide.runPythonAsync(code);
    </script>
  </body>
</html>

SDK integration

Inside your main.py, import Wavedash from the js module. Pass Python dicts (SDK config, etc.) through to_js(..., dict_converter=Object.fromEntries) so JavaScript receives a plain object — plain to_js produces a Map, which the SDK ignores.

Calling Wavedash.init() is required. Your game stays hidden behind the Wavedash loading screen until you do. Call it once your game is ready to play.

from js import Object, Wavedash, document, window
from pyodide.ffi import create_proxy, to_js

# Pyodide and the standard library have finished loading (~10 MB of WASM).
Wavedash.updateLoadProgressZeroToOne(0.5)

canvas = document.getElementById("gameCanvas")
ctx = canvas.getContext("2d")

def game_loop(now):
    # update + draw
    window.requestAnimationFrame(game_loop_proxy)

# Keep a proxy alive: Pyodide destroys a bare function passed to JS after one call.
game_loop_proxy = create_proxy(game_loop)

# ...set up input handlers...
window.requestAnimationFrame(game_loop_proxy)

# Game state and input handlers are ready — signal fully loaded.
Wavedash.updateLoadProgressZeroToOne(1.0)
Wavedash.init(to_js({"debug": True}, dict_converter=Object.fromEntries))

All SDK methods work through this same bridge — Wavedash.setAchievement("first_win", True), Wavedash.requestStats(), and so on. See SDK setup and Functions reference for the full list.

wavedash.toml

game_id = "YOUR_GAME_ID_HERE"
upload_dir = "./web"
entrypoint = "index.html"

Pyodide is about 10 MB of WebAssembly plus standard-library bundles. It's great for prototypes and turn-based games, but if you're targeting 60fps action, budget load time carefully and use updateLoadProgressZeroToOne to keep the player oriented while Pyodide boots.