Orange Bricks LogoOrange Bricks

Building Custom JupyterLab Extensions: A Developer's Guide

Tutorial
December 28, 2024
12 min read

Step-by-step tutorial on creating your first JupyterLab extension, from setup to publishing on npm.

Building Custom JupyterLab Extensions: A Developer's Guide

Creating custom JupyterLab extensions can significantly enhance your team's productivity. This comprehensive guide will walk you through building your first extension from scratch.

Prerequisites

Before starting, ensure you have:

  • Node.js (version 14 or higher)
  • Python (version 3.8 or higher)
  • JupyterLab installed
  • Basic knowledge of TypeScript and React

Setting Up Your Development Environment

1. Install Required Tools

pip install jupyterlab cookiecutter
npm install -g yarn

2. Create Extension Template

cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts

This will prompt you for:

  • Extension name
  • Author information
  • License type
  • Python package name

3. Development Installation

cd your-extension-name
pip install -e .
jupyter labextension develop . --overwrite

Understanding the Extension Structure

A typical JupyterLab extension includes:

your-extension/
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ index.ts          # Main entry point
│   └── widget.tsx        # React components
ā”œā”€ā”€ package.json          # NPM configuration
ā”œā”€ā”€ pyproject.toml        # Python configuration
ā”œā”€ā”€ install.json          # JupyterLab configuration
└── tsconfig.json         # TypeScript configuration

Building Your First Extension

Let's create a simple "Hello World" extension that adds a new menu item.

1. Update the Main Index File

// src/index.ts
import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { ICommandPalette } from '@jupyterlab/apputils';

const plugin: JupyterFrontEndPlugin<void> = {
  id: 'hello-world:plugin',
  autoStart: true,
  requires: [ICommandPalette],
  activate: (app: JupyterFrontEnd, palette: ICommandPalette) => {
    const { commands } = app;
    const command = 'hello-world:open';

    commands.addCommand(command, {
      label: 'Say Hello',
      caption: 'Display a hello message',
      execute: () => {
        console.log('Hello from JupyterLab extension!');
      }
    });

    palette.addItem({ command, category: 'Tutorial' });
  }
};

export default plugin;

2. Build and Test

jlpm build
jupyter lab build --dev-build=False --minimize=False

Start JupyterLab and look for your command in the command palette (Ctrl+Shift+C).

Advanced Extension Features

Adding UI Components

Create interactive widgets using React:

// src/widget.tsx
import React, { useState } from 'react';
import { ReactWidget } from '@jupyterlab/apputils';

const HelloWidget = () => {
  const [count, setCount] = useState(0);

  return (
    <div style={{ padding: '20px' }}>
      <h2>Hello JupyterLab!</h2>
      <p>Button clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me!
      </button>
    </div>
  );
};

export class HelloReactWidget extends ReactWidget {
  render(): JSX.Element {
    return <HelloWidget />;
  }
}

Integrating with Notebook Cells

Access and manipulate notebook content:

import { INotebookTracker } from '@jupyterlab/notebook';

const processNotebook = (tracker: INotebookTracker) => {
  const notebook = tracker.currentWidget;
  if (!notebook) return;

  const cells = notebook.content.widgets;
  cells.forEach((cell, index) => {
    console.log(`Cell ${index}:`, cell.model.value.text);
  });
};

Adding Settings

Allow users to configure your extension:

// schema/plugin.json
{
  "jupyter.lab.setting-icon": "ui-components:palette",
  "jupyter.lab.setting-icon-label": "Hello World Settings",
  "title": "Hello World",
  "description": "Settings for the Hello World extension",
  "type": "object",
  "properties": {
    "greeting": {
      "type": "string",
      "title": "Greeting Message",
      "description": "Custom greeting message",
      "default": "Hello"
    }
  }
}

Testing Your Extension

Unit Testing

// tests/hello.spec.ts
import { test, expect } from '@playwright/test';

test('hello world extension loads', async ({ page }) => {
  await page.goto('/lab');
  await page.waitForSelector('[data-id="hello-world:plugin"]');
  expect(await page.isVisible('[data-id="hello-world:plugin"]')).toBe(true);
});

Manual Testing

  1. Install in development mode
  2. Test all features thoroughly
  3. Check browser console for errors
  4. Verify compatibility with different JupyterLab versions

Publishing Your Extension

1. Prepare for Release

# Update version
npm version patch

# Build production version
jlpm build:prod

2. Publish to PyPI and NPM

# Build Python package
python -m build

# Upload to PyPI
twine upload dist/*

# Publish to NPM
npm publish

3. Add to conda-forge (optional)

Submit a PR to conda-forge for easier installation.

Best Practices

Code Quality

  • Use TypeScript for type safety
  • Follow JupyterLab's coding standards
  • Add comprehensive tests
  • Document your API

User Experience

  • Provide clear error messages
  • Add helpful tooltips and documentation
  • Follow JupyterLab's UI patterns
  • Support keyboard shortcuts

Performance

  • Lazy load heavy components
  • Debounce expensive operations
  • Minimize bundle size
  • Cache computed values

Common Pitfalls

  1. Not handling async operations properly
  2. Forgetting to dispose of event listeners
  3. Ignoring accessibility requirements
  4. Poor error handling
  5. Not testing across different environments

Resources for Further Learning

Building JupyterLab extensions opens up endless possibilities for customizing and enhancing your data science workflow. Start small, iterate quickly, and don't hesitate to contribute back to the community!

Ready to transform your notebooks?

Try Auto Dashboards and start creating interactive dashboards from your Jupyter notebooks with just one click.