Calling Browser APIs from Python with PyScript's js Module
Learn how to use PyScript’s built‑in js module to call browser APIs such as fetch and DOM methods directly from Python, with a worked example, setup steps, and performance considerations.
23 Dec 2025, 18:23 UTC

Problem: You need to interact with the browser from Python without leaving the PyScript runtime
When building interactive demos or lightweight internal tools with PyScript, you often reach a point where pure‑Python logic must read or modify the DOM, make network requests, or trigger browser‑only APIs (e.g., alert, fetch, localStorage). Writing the same logic in JavaScript defeats the purpose of staying in Python, while copying snippets between languages creates maintenance overhead.
Thesis: PyScript’s built‑in js module provides a direct bridge to the global window object, letting you call browser APIs as if they were native Python functions.
How the bridge works
Inside any <py-script> block, the name js is already bound to a proxy object that forwards attribute accesses and calls to the corresponding JavaScript entity. For example, js.document refers to window.document, and js.fetch maps to the browser’s Fetch API. The proxy handles the conversion of basic Python types (str, int, list, dict) to their JavaScript equivalents and wraps return values in Python‑friendly objects when possible.
Setup
- Include the PyScript runtime from the official CDN in your HTML
<head>:<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" /> <script defer src="https://pyscript.net/latest/pyscript.js"></script> - Place your Python logic inside a
<py-script>block. No additional installation is required; thejsobject is available by default.
Worked example: Fetch JSON and display it
The following snippet shows a button that, when clicked, runs a Python function which uses js.fetch to retrieve data from a public API and writes the result into a <div> without reloading the page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PyScript js demo</title>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<h2>PyScript + js fetch example</h2>
<button id="loadBtn">Load user data</button>
<div id="output" style="margin-top:1rem; white-space:pre-wrap;"></div>
<py-script>
from pyscript import js, display
async def load_user():
try:
# js.fetch returns a Promise; await works because PyScript wraps it
response = await js.fetch('https://api.github.com/users/octocat')
if not response.ok:
raise Exception(f'HTTP {response.status}')
data = await response.json()
# Convert the JS object to a Python dict for easy formatting
user = js.Object.fromEntries(data.entries()) if hasattr(data, 'entries') else data
# Show a subset of fields
output = f"Login: {user.get('login')}\n" \
f"ID: {user.get('id')}\n" \
f"Public repos: {user.get('public_repos')}"
js.document.getElementById('output').innerText = output
except Exception as e:
js.alert(f'Error: {e}')
# Bind the click handler – note we pass the Python callable directly
js.document.getElementById('loadBtn').onclick = load_user
</py-script>
</body>
</html>
To try this example:
- Save the markup to a file, e.g.,
pyscript_js_demo.html. - Open the file in a modern browser (Chrome, Firefox, Edge, Safari). No server is required; the file can be opened via
file://. - Click the “Load user data” button. If the bridge works, you will see the user’s login, ID, and public repo count appear in the
#outputdiv, or an alert if something goes wrong.
Verification steps
- Open the browser’s developer console (F12) and reload the page. Look for any red error messages; a successful run will show none.
- To observe the call overhead, wrap a js DOM update in
performance.now()calls and compare the delta to a vanilla JavaScript equivalent. Expect a few hundred microseconds extra per call due to the WebAssembly bridge. - Check that the
jsobject is present by typingjsin the console while the page is running; it should display a proxy object.
Trade‑offs and limitations
- Performance: Each invocation crosses the Python‑WebAssembly‑JavaScript boundary, adding latency. For high‑frequency UI updates (e.g., animation frames), prefer writing the hot path in JavaScript and calling into Python only when needed.
- Extension modules: The
jsbridge works only with pure‑Python code. Packages that rely on compiled C extensions (e.g.,numpy,pandas) cannot be used inside a<py-script>block that also makesjscalls. - Error context: Exceptions thrown in JavaScript callbacks are surfaced as Python exceptions, but the original JavaScript stack trace may be truncated, making debugging harder.
Actionable closing
If your goal is to keep the bulk of your logic in Python while still accessing occasional browser features, the js module is the simplest path. Start with small, isolated calls (like a fetch or a DOM query) and measure their impact on responsiveness. For performance‑critical loops, consider moving those loops to JavaScript and exposing a thin Python‑callable wrapper via js. This approach gives you the readability of Python where it matters most, without sacrificing the ability to reach the browser’s full API set when necessary.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.