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:

LayerLanguagePurposeExample
FrontendTypeScript/JSUI widgets, commands, keybindingsSyntax theme, TOC panel
ServerPythonNew REST endpoints, background tasksGit 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 directory
  • pyproject.toml — unified build config using hatch-jupyter-builder
  • tsconfig.json and package.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

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:

  1. Run jupyter lab build --minimize=False to surface deprecation warnings.
  2. Update @jupyterlab/* dependencies in package.json.
  3. Replace deprecated tokens (e.g., IMainMenu API changes).
  4. 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

BenefitCost
Deep UI customisationMust learn TypeScript + Lumino
Prebuilt distribution is simple for usersBuild tooling is complex for maintainers
Server extensions enable backend logicTwo codebases to maintain (TS + Python)
Large community ecosystemQuality 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.

pythonjupyterproductivity

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.