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.

This post was written in 2024 and has not been updated since (up to fixing some typos and broken links).

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:

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: