JupyterLab Extensions — Deep Dive
Architecture of JupyterLab extensions
JupyterLab’s frontend is a single-page application built on Lumino (formerly PhosphorJS), a widget library that provides a dockable panel layout, command palette, keyboard shortcut system, and menu bar. Every visible component — the file browser, the notebook panel, the terminal — is a Lumino widget registered through JupyterLab’s extension API.
An extension is a JavaScript (or TypeScript) module that exports a JupyterFrontEndPlugin object. This object declares:
id— A unique string identifier.autoStart— Whether the extension activates on lab startup.requires/optional— Tokens for dependency injection (e.g.,INotebookTracker,ISettingRegistry).activate(app, ...deps)— A function that receives the application instance and requested dependencies.
const plugin: JupyterFrontEndPlugin<void> = {
id: 'my-org:cell-timer',
autoStart: true,
requires: [INotebookTracker],
activate: (app: JupyterFrontEnd, tracker: INotebookTracker) => {
tracker.widgetAdded.connect((_, panel) => {
// hook into notebook lifecycle
});
}
};
export default plugin;
The dependency-injection approach means extensions can compose: your extension can depend on jupyterlab-lsp’s token and extend its diagnostics panel, for example.
Server extensions vs frontend extensions
JupyterLab extensions come in two layers:
| Layer | Language | Purpose | Example |
|---|---|---|---|
| Frontend | TypeScript/JS | UI widgets, commands, keybindings | Syntax theme, TOC panel |
| Server | Python | New REST endpoints, background tasks | Git operations, LSP proxy |
Many extensions ship both. jupyterlab-git, for example, has a Python server extension that calls git commands and a frontend extension that renders the diff viewer. The pyproject.toml build system introduced in JupyterLab 3+ bundles both layers into a single pip-installable package.
Building a custom extension from scratch
Scaffolding
Use the official cookiecutter template:
pip install cookiecutter
cookiecutter https://github.com/jupyterlab/extension-template
The template generates:
src/index.ts— frontend entry point<package_name>/— Python server extension directorypyproject.toml— unified build config usinghatch-jupyter-buildertsconfig.jsonandpackage.json— TypeScript compilation settings
Development loop
# Install in dev mode (builds frontend and links server extension)
pip install -e ".[dev]"
# Watch for TypeScript changes and rebuild automatically
jlpm watch
# In another terminal, start JupyterLab in watch mode
jupyter lab --autoreload
Changes to TypeScript files trigger a rebuild; refreshing the browser picks up the new code. Server extension changes require restarting the lab process.
Testing
Frontend tests use Jest via the @jupyterlab/testutils package. Integration tests use Galata, a Playwright-based framework that launches a real JupyterLab instance and interacts with it programmatically:
import { test, expect } from '@jupyterlab/galata';
test('should show timer badge after cell execution', async ({ page }) => {
await page.notebook.createNew();
await page.notebook.setCell(0, 'code', 'import time; time.sleep(1)');
await page.notebook.runCell(0);
const badge = page.locator('.cell-timer-badge');
await expect(badge).toBeVisible();
const text = await badge.textContent();
expect(parseFloat(text!)).toBeGreaterThan(0.9);
});
Galata tests catch real rendering regressions that unit tests miss.
Publishing and distribution
Prebuilt distribution (recommended)
Package the extension so end users only need pip install:
python -m build # produces sdist + wheel
twine upload dist/* # publish to PyPI
The wheel includes the compiled JavaScript bundle in labextensions/. No Node.js required on the user’s machine.
Conda-forge
For scientific teams that use conda, submit a recipe to conda-forge. The recipe typically wraps the PyPI package. Conda-forge’s CI builds and tests the package on Linux, macOS, and Windows.
Real-world extension patterns
Custom cell metadata toolbar
Enterprise teams often need to tag cells with metadata — “reviewed”, “approved”, “PII-sensitive”. A custom extension can add a toolbar button to each cell that writes structured metadata to the cell’s JSON, which downstream compliance tools can audit.
Shared workspace settings
ISettingRegistry lets extensions declare JSON Schema settings that users modify via the Settings Editor. For team-wide defaults, deploy a overrides.json file in the JupyterLab configuration directory:
{
"my-org:cell-timer": {
"showMilliseconds": false,
"warningThresholdSeconds": 30
}
}
Kernel-aware extensions
Extensions that need to execute code in the user’s kernel (e.g., a variable inspector) use IKernelConnection.requestExecute(). This sends code to the active kernel and receives results via the Jupyter messaging protocol. Be cautious: executing hidden code can confuse users and pollute namespace.
Maintenance and upgrades
JupyterLab major versions (3 → 4) can break extension APIs. The migration path:
- Run
jupyter lab build --minimize=Falseto surface deprecation warnings. - Update
@jupyterlab/*dependencies inpackage.json. - Replace deprecated tokens (e.g.,
IMainMenuAPI changes). - Re-run Galata integration tests.
Pin your extension’s JupyterLab compatibility range in pyproject.toml:
[project]
dependencies = ["jupyterlab>=4.0,<5.0"]
This prevents pip from installing the extension into incompatible environments.
Tradeoffs
| Benefit | Cost |
|---|---|
| Deep UI customisation | Must learn TypeScript + Lumino |
| Prebuilt distribution is simple for users | Build tooling is complex for maintainers |
| Server extensions enable backend logic | Two codebases to maintain (TS + Python) |
| Large community ecosystem | Quality varies — audit before adopting |
One thing to remember: JupyterLab’s extension system is powerful enough to turn a generic notebook tool into a domain-specific IDE. The investment in learning Lumino and the extension API pays off whenever your team needs a workflow that no existing tool provides.
See Also
- Python Jupyter Notebooks See why Jupyter feels like a digital lab bench where you can test ideas and learn fast.
- Ci Cd Why big apps can ship updates every day without turning your phone into a glitchy mess — CI/CD is the behind-the-scenes quality gate and delivery truck.
- Containerization Why does software that works on your computer break on everyone else's? Containers fix that — and they're why Netflix can deploy 100 updates a day without the site going down.
- Python 310 New Features Python 3.10 gave programmers a shape-sorting machine, friendlier error messages, and cleaner ways to say 'this or that' in type hints.
- Python 311 New Features Python 3.11 made everything faster, error messages smarter, and let you catch several mistakes at once instead of stopping at the first one.