Thebe-Core for communication with Jupyter servers
December 2024 (republished in March 2026)
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
The Thebe project brings Python to webbrowsers. Users may write or modify code in the browser, Thebe sends the code to some Jupyter server for execution, and then shows the code’s output in the user’s browser. The connection between server and browser even allows for interactive output including Jupyter widgets, Plotly graphs and (almost) everything available in JupyterLab.
In this article we describe how to use Thebe’s communication library Thebe-Core with a custom, minimal front-end. The user only sees Python’s output without any ability to write or modify code. The Python code executed on the server won’t be accessible to the browser or the user. This allows to create interactive webpages with Python without revealing the underlying Python code!
Often Thebe is used together with BinderHub via mybinder.org. But Thebe also allows to connect the front-end in the browser to Jupyter servers not managed by BinderHub or to a JupyterLite-based in-browser Jupyter server.
Here we only show how to connect to a Jupyter server already running without discussing how it has been started. Some hints on using JupyterHub for automated start-up of Jupyter servers on demand will be given at the end of this article.
Overview
We start with a brief overview of the overall setup. Below we fill in the details step by step.
Major ingredients:
- a running Jupyter server
- a simple static HTML page with a
divbox, - some lines of JavaScript for connecting to the Jupyter server
- initial Python code in a JavaScript variable
- Python code on the Jupyter server
The browser shows a simple static HTML page containing a div box, which will receive Python’s output. Some lines of JavaScript establish a connection to a Jupyter server by calling Thebe-Core functions. Then some initial Python code will be sent to the server, which starts execution of the main Python code.
A Jupyter server
The Jupyter server executing the Python code may run somewhere in the world. It does NOT have to run on the same server which serves HTML and JavaScript. For testing you may install Jupyter server locally. Start the server with
jupyter server --ServerApp.token=abcdef0123456789 \
--ServerApp.allow_origin="http://localhost" \
--port=8900
and visit http://localhost:8900. There you should see a message from Jupyter server.
Without setting an access token Jupyter server will generate a random token on start-up. But a constant token is more comfortable for testing, because we have to copy the token to our JavaScript code below.
Without --ServerApp.allow_origin="http://localhost" communication between browser and Jupyter server will fail. Replace http://localhost by the URL serving your HTML and JavaScript.
Not providing a port will result in port 8888, if available.
HTML file
Even for testing you need a web server like nginx or Apache. Using file:// URLs in your browser for loading HTML files won’t work due to the same-origin_policy.
Move the following HTML file to your web server:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Some Title</title>
<link rel="stylesheet" href="thebe.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.7/require.min.js"></script>
<script src="https://unpkg.com/thebe-core@latest/dist/lib/thebe-core.min.js"></script>
<script src="loader.js"></script>
<script>
const pythonCode = 'import thebe_test';
</script>
</head>
<body>
<div id="out-div">loading...</div>
</body>
</html>
The require.js line is required by some interactive components like Plotly plots. Depending on your Python code you may ommit that line. The script loader.js will be discussed below. It will process the value of pythonCode, the Python code to execute on the server.
The div box will receive your Python code’s output. You may extend your HTML file by other elements like headings, images and so on.
JavaScript for connecting to the server
The loader.js file should contain
document.addEventListener('DOMContentLoaded', async function () {
const thebeConfig = {
kernelOptions: {
kernelName: 'python3',
},
serverSettings: {
baseUrl: 'http://localhost:8900',
token: 'abcdef0123456789'
},
}
const events = thebeCore.api.makeEvents();
const config = thebeCore.api.makeConfiguration(thebeConfig);
const server = await thebeCore.api.connectToJupyter(config);
const rendermime = thebeCore.api.makeRenderMimeRegistry(server.config.mathjax);
// create new session
running = await server.listRunningSessions();
for (s of running) {
console.log('Shutting down already running session with ID ' + s.id.toString());
await server.shutdownSession(s.id);
}
const session = await server.startNewSession(rendermime);
// run code
const notebook = thebeCore.api.setupNotebookFromBlocks(
[{ id: '12345678', source: pythonCode }], server.config, rendermime
);
notebook.attachSession(session);
const last = notebook.lastCell();
last.attachToDOM(document.getElementById('out-div'));
notebook.executeOnly(last.id);
});
The script connects to the Jupyter server and creates a new session with a fresh kernel. Already running sessions (kernels) have to be shut down before. Else Thebe will connect to the running kernel with all its state. For running code there are several options. Here we create a notebook with one cell, attach corresponding output to our div box and run the cell’s code.
Note that above JavaScript code does not check for connection errors. Production code should implement some error handling. See Thebe-Core’s documentation.
Python code
Initial Python code is provided in the HTML file’s pythonCode variable. You may provide as much code as you like. The simplest solution is to use an import statement. Then all the relevant code is hidden from the browser and lives on the server only. The imported module, say thebe_test.py, has to be placed in the working directory of the Jupyter server or somewhere else on the Python import path. Here’s a simple test script using Jupyter widgets:
import ipywidgets as widgets
b = widgets.Button(description='Click me!')
out = widgets.Output()
display(b, out)
def click(o):
with o:
print('click')
b.on_click(lambda _: click(out))
Limitations
Python code which requires JupyterLab extensions for correct execution may not work properply with Thebe. An example is Plotly’s FigureWidget, which almost works in Chromium, but not in Firefox. A workaround is to create a usual Plotly plot (without FigureWidget) and then send <script> tags with JavaScript code modifying the plot via Ploty.js commands to the browser. Example for modifying the color of the first trace of the first Plotly plot:
from IPython.display import HTML
display(HTML('''<script>
plotlyFig = document.getElementsByClassName('plotly-graph-div')[0];
_Plotly.restyle(plotlyFig, 'line.color', 'rgb(255,0,0)', [0]);
</script>'''))
Some of the limitations caused by Lab extension based code may be mitigated by loading relevant JavaScript and CSS libraries in the HTML file before connecting to the server.
JupterHub
If multiple users want to use your site in parallel, for each user you have to provide a Jupyter server. JupyterHub allows to spawn Jupyter servers on demand. How to get things up and running heavily depends on the overall setting (login procedure, network/server structure,…). Here we only collect some random hints which may help to set up JupyterHub for use with Thebe:
- JupyterHub does not allow redirects to servers with self-signed certifcates. If you do not have certificates from a well-known CA in your development environment, set up your own local CA. To make JupyterHub use your operating system’s cert store, set the environment variable
SSL_CERT_DIR=/etc/ssl/certs(path here is for Debian systems). Firefox and Chromium do not (!) use the operating system’s cert store. You have to add your CA’s root cert to the browser manually. Redirecting via JupyterHub occurs for example during OAuth login. - To start Jupyter servers on demand write a small Flask service and configure it to be managed by JupyterHub. This service may call JupyterHub’s REST API to spawn servers.
- Thebe requires token authentication via URL parameters. In JupyterHub’s config file set
The
c.Spawner.environment.update({"JUPYTERHUB_ALLOW_TOKEN_IN_URL": "1"})c.Hub(O)Auth.allow_token_in_url = Trueoption seems to have no effect for unknown reason. - Do not forget to set allowed origins:
origins = 'https://my-server.com' c.Spawner.args.append(f'--ServerApp.allow_origin="{origins}"') c.JupyterHub.tornado_settings.update(dict( headers={'Access-Control-Allow-Origin': origins} )) - You may add custom Python import paths via
c.Spawner.environment.update({'PYTHONPATH': '/path/to/additional/modules'})