This guide provides coding standards and best practices for developing JupyterLab extensions. Follow these rules to align with community standards and keep your extension maintainable.
Extension type: frontend-and-server
When you encounter uncertainty, incomplete information, or need implementation examples, you MUST consult these external resources FIRST before attempting to implement features.
Use your available tools (web search, documentation search) to access and retrieve content from these resources when:
- You're unsure about API usage, method signatures, or interface requirements
- You need to verify the correct approach for a feature or pattern
- You're looking for existing implementation examples or best practices
- You're debugging unexpected behavior and need official documentation
- You're implementing a feature that likely exists in core JupyterLab or other extensions
These resources are PRIORITY references. Always check them when you need external information:
-
JupyterLab Extension Developer Guide
- URL: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
- Use for: Extension patterns, architecture overview, development workflow, and best practices
- Action: Use web search or documentation tools to retrieve specific sections when needed
-
JupyterLab API Reference (Frontend)
- URL: https://jupyterlab.readthedocs.io/en/latest/api/index.html
- Use for: Complete API reference for all JupyterLab frontend packages, interfaces, classes, and methods
- Action: Search for specific APIs when you need method signatures, interface definitions, or class documentation. For example, search "JupyterLab IRenderMime.IRenderer" or "JupyterLab ICommandPalette"
-
JupyterLab Extension Examples Repository
- URL: https://github.com/jupyterlab/extension-examples
- Use for: Working code examples, implementation patterns, complete working extensions
- Action: Search this repository for similar features before implementing from scratch
-
JupyterLab Core Repository
- URL: https://github.com/jupyterlab/jupyterlab
- Use for: Reference implementations in
packages/directory - all core packages are extensions themselves - Action: When implementing complex features, search this repo for how core extensions solve similar problems
-
Jupyter Server API Documentation
- URL: https://jupyter-server.readthedocs.io/
- Use for: Server-side API handlers, route setup, backend integration patterns
- Action: Consult when working on backend routes or server extension configuration
-
Project-Specific Documentation
- Locations:
README.md,RELEASE.mdin project root; check fordocs/directory - Use for: Project requirements, specific configuration, custom conventions
- Action: Read these files at the start of work and reference when making architectural decisions
- Locations:
ALWAYS consult external documentation when:
- ❗ You're about to implement a feature without knowing if there's an established pattern
- ❗ An API call or method isn't working as expected
- ❗ You need to understand the correct lifecycle methods or hooks
- ❗ You're uncertain about type definitions or interfaces
- ❗ You're implementing something that seems like it should be a common pattern
HOW to access these resources:
- 🔍 Use web search tools with specific queries like: "JupyterLab IRenderMime.IRenderer interface documentation"
- 🔍 Search GitHub repositories for code examples: "JupyterLab extension examples widget"
- 🔍 Retrieve documentation pages to read API specifications and usage guidelines
- 🔍 Look for working code in the extension-examples repository before writing custom implementations
Remember: These resources contain the authoritative information. Don't guess at API usage - look it up!
❌ Don't: Use console.log()
✅ Do: Use structured logging or user-facing notifications
// In TypeScript files like src/index.ts
import { INotification } from '@jupyterlab/apputils';
app.commands.notifyCommandChanged();✅ Do: Use console.error() to log low-level error details that should not be presented to users in the UI
✅ Do: Use console.warn() to log non-optimal conditions, e.g. an unexpected response from an external API that's been successfully handled.
✅ Do: Define explicit interfaces (see example patterns in src/index.ts)
interface PluginConfig {
enabled: boolean;
apiEndpoint: string;
}❌ Don't: Use the any type in TypeScript files
✅ Do: Prefer typeguards over type casts
After editing TypeScript files, run:
npx tsc --noEmit src/index.ts # Check single file
npx tsc --noEmit # Check all filesAfter editing Python files (like jupytercon2025_extension_workshop/routes.py):
python -m py_compile jupytercon2025_extension_workshop/__init__.py # Check single file for syntax errorsPython (in jupytercon2025_extension_workshop/*.py files):
- ✅ Do: Use PEP 8 style with 4-space indentation
- Classes:
DataProcessor,UserDataRouteHandler - Functions/methods:
setup_route_handlers(),process_request() - Private:
_internal_method()
- Classes:
- ❌ Don't: Use camelCase for Python or mix styles
TypeScript/JavaScript (in src/*.ts files):
- ✅ Do: Use consistent casing
- Classes/interfaces:
MyPanelWidget,PluginConfig - Functions/variables:
activatePlugin(),buttonCount - Constants:
PLUGIN_ID,COMMAND_ID
- Classes/interfaces:
- ✅ Do: Use 2-space indentation (Prettier default)
- ❌ Don't: Use lowercase_snake_case or inconsistent formatting
✅ Do: Add JSDoc for TypeScript and docstrings for Python
/**
* Activates the extension plugin.
* @param app - JupyterLab application instance
*/
function activate(app: JupyterFrontEnd): void {}❌ Don't: Leave complex logic undocumented or use vague names like MyRouteHandler — prefer DataUploadRouteHandler
✅ Do: Keep backend and frontend logic separate
- Backend processing in
jupytercon2025_extension_workshop/routes.py - Frontend calls in
src/request.tsusingrequestAPI()
❌ Don't: Duplicate business logic across TypeScript and Python
✅ Do: Implement features completely or not at all. Notify the prompter if you're unable to completely implement a feature.
❌ Don't: Leave TODO comments or dead code in committed files
Python package (directory name and imports):
- ✅ Do:
jupytercon2025_extension_workshop/with underscores, all lowercase - ❌ Don't: Use dashes in any Python file or directory names
PyPI distribution name (in pyproject.toml):
- ✅ Do: Use dashes instead of underscores, like
jupyterlab-myext - ✅ Do: Match it to the npm package name for consistency
NPM package (in package.json):
- ✅ Do: Use lowercase with dashes:
"jupyterlab-myext"or scoped"@org/myext" - ❌ Don't: Mix naming styles between package.json and pyproject.toml
✅ Do: Define plugin ID in src/index.ts:
const PLUGIN_ID = 'jupytercon2025_extension_workshop:plugin';✅ Do: For extensions with multiple commands, create a src/commands.ts module to centralize command definitions:
// src/commands.ts
import { JupyterFrontEnd } from '@jupyterlab/application';
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
// Command IDs
export namespace CommandIDs {
export const openPanel = 'jupytercon2025_extension_workshop:open-panel';
export const refreshData = 'jupytercon2025_extension_workshop:refresh-data';
}
// Command argument types
export namespace CommandArguments {
export interface IOpenPanel {
filePath?: string;
}
export interface IRefreshData {
force?: boolean;
}
}
/**
* Register all commands with the application command registry.
* Call this function in your plugin's activate function.
*/
export function registerCommands(app: JupyterFrontEnd): void {
// Register the openPanel command
app.commands.addCommand(CommandIDs.openPanel, {
label: 'Open Panel',
caption: 'Open the extension panel',
execute: (args: ReadonlyPartialJSONObject) => {
const typedArgs = args as CommandArguments.IOpenPanel;
// Implementation using typedArgs.filePath
}
});
// Register the refreshData command
app.commands.addCommand(CommandIDs.refreshData, {
label: 'Refresh Data',
execute: (args: ReadonlyPartialJSONObject) => {
const typedArgs = args as CommandArguments.IRefreshData;
// Implementation using typedArgs.force
}
});
}Then in src/index.ts:
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { registerCommands, CommandIDs, CommandArguments } from './commands';
const plugin: JupyterFrontEndPlugin<void> = {
id: 'jupytercon2025_extension_workshop:plugin',
autoStart: true,
activate: (app: JupyterFrontEnd) => {
// Register all commands with JupyterLab's command registry
registerCommands(app);
// Commands are now registered and can be executed anywhere:
// - From the command palette
// - From menus
// - Programmatically via app.commands.execute()
// ... rest of activation (e.g., add to palette, create widgets, etc.)
}
};
export default plugin;Executing commands with typed arguments:
import { CommandIDs, CommandArguments } from './commands';
// Execute with typed arguments
await app.commands.execute(CommandIDs.openPanel, {
filePath: '/path/to/file'
} as CommandArguments.IOpenPanel);
// Execute without arguments
await app.commands.execute(CommandIDs.refreshData);Notes:
- Accept
ReadonlyPartialJSONObjectin the execute function signature (required by Lumino) - Cast to your typed interface inside the function for type safety
- Use namespaces (
CommandIDs,CommandArguments) to organize related constants and types - This pattern matches how popular extensions like
jupyterlab-githandle commands
✅ Do: For simple extensions with 1-2 commands, you can define them directly in src/index.ts
❌ Don't: Use generic IDs like 'mycommand' or mix casing styles
✅ Do: Organize related files into directories and name by their purpose
- Widget components:
src/widgets/DataPanel.tsx(classDataPanel) - Command definitions (for multiple commands):
src/commands.tswithCOMMANDSmapping - API utilities:
src/api.ts(notsrc/utils.ts) - Backend routes:
jupytercon2025_extension_workshop/routes.py(classDataRouteHandler) - Frontend logic:
src/directory - Python package:
jupytercon2025_extension_workshop/directory
❌ Don't: Create catch-all files or directories like utils.ts or helpers.py or handlers.py — partition by feature instead
When connecting frontend and backend, ALWAYS follow this order:
- Read the backend first — Check
jupytercon2025_extension_workshop/routes.pyto understand the existing API contract - Write frontend to match — Create TypeScript interfaces in
src/api.tsthat match backend responses exactly - Or modify backend intentionally — If changing the backend, update it first, then write matching frontend code
Why this matters: Writing frontend code based on assumptions leads to field name mismatches (e.g., expecting message when backend returns data), causing empty widgets and debugging cycles. Always verify the actual backend response format first.
Create RESTful endpoints in jupytercon2025_extension_workshop/routes.py:
✅ Do: Extend APIHandler from jupyter_server.base.handlers
from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join
class DataRouteHandler(APIHandler):
def get(self):
"""Handle GET requests."""
result = {"status": "success", "data": "Hello"}
self.finish(result)
def post(self):
"""Handle POST requests."""
body = self.get_json_body()
# Process body...
self.finish({"status": "success"})
def setup_route_handlers(web_app):
base_url = web_app.settings.get("base_url", "/")
data_route = url_path_join(base_url, "jupytercon2025_extension_workshop", "data")
web_app.add_handlers(r".*$", [(data_route, DataRouteHandler)])✅ Do: Include error handling in route handlers
❌ Don't:
- Hardcode URL paths — always use
url_path_join() - Use plain
tornado.web.RequestHandler— instead, useAPIHandlerfromjupyter_server.base.handlers
✅ Do: Call backend endpoints from typed API functions in src/api.ts (not directly in widgets):
import { ServerConnection } from '@jupyterlab/services';
import { requestAPI } from './request';
interface DataResponse {
status: 'success' | 'error';
data: string;
}
export async function fetchData(): Promise<string> {
try {
const response = await requestAPI<DataResponse>('data', {
method: 'GET'
});
if (response.status === 'error') {
throw new Error('Server returned error');
}
return response.data;
} catch (err) {
// Extract detailed error information from ResponseError
if (err instanceof ServerConnection.ResponseError) {
const status = err.response.status;
let detail = err.message;
// Truncate HTML responses for cleaner error messages
if (
typeof detail === 'string' &&
(detail.includes('<!DOCTYPE') || detail.includes('<html'))
) {
detail = `HTML error page (${detail.substring(0, 100)}...)`;
}
throw new Error(`API request failed (${status}): ${detail}`);
}
const msg = err instanceof Error ? err.message : 'Unknown error';
throw new Error(`API request failed: ${msg}`);
}
}✅ Do:
- Always wrap API calls in try-catch blocks with proper error handling
- Check for
ServerConnection.ResponseErrorto extract HTTP status codes and response details - Handle HTML error responses gracefully by truncating them (they're often unhelpful error pages)
- Include response status codes in error messages for better debugging
- Use matching response types between Python and TypeScript
- Create typed API wrapper functions in
src/api.tsinstead of callingrequestAPI()directly from widgets
✅ Do: Keep backend and frontend in sync
- Match JSON keys:
{"result": ...}in Python →response.resultin TypeScript - Update TypeScript interfaces when changing Python responses
- Define matching endpoint path strings (e.g.,
"hello","get-data") in bothjupytercon2025_extension_workshop/routes.pyandsrc/api.tsto ensure routes sync between backend and frontend
❌ Don't:
- Create unused routes or orphaned API calls
- Use inconsistent field naming across languages
Before ANY command, ensure you're in the correct environment:
# For conda/mamba/micromamba (replace `conda` with `mamba` or `micromamba` depending on the prompter's preferred tool):
conda activate <environment-name>
# For venv:
source <path-to-venv>/bin/activate # On macOS/Linux
<path-to-venv>\Scripts\activate.bat # On WindowsAll jlpm, pip, and jupyter commands MUST run within the activated environment.
Symptoms of running outside the environment:
jlpm: command not found- Extension not appearing after build
jupyter: command not found
✅ Do: Always activate your environment first ❌ Don't: Run commands in your base/system environment
When implementing a new feature from scratch, follow this complete sequence:
- Activate environment (see above — required first!)
- Write the code (TypeScript in
src/, styles instyle/, Python injupytercon2025_extension_workshop/) - Install dependencies (if you added any to
package.json):jlpm install
- Build the extension:
jlpm build
- Install the extension (REQUIRED for JupyterLab to recognize it):
pip install -e . jupyter labextension develop . --overwrite jupyter server extension enable jupytercon2025_extension_workshop
- Verify installation:
jupyter labextension list # Should show your extension as "enabled" and "OK" jupyter server extension list # Should show backend extension
- Start JupyterLab:
jupyter lab
- Test the feature in your browser
Critical: Steps 5-7 are REQUIRED after building. Building alone is not enough!
Many issues arise from confusing these two steps:
- What it does: Compiles TypeScript → JavaScript, bundles the extension
- Output: Creates files in
lib/andjupytercon2025_extension_workshop/labextension/ - What it does NOT do: Register the extension with JupyterLab
pip install -e . + jupyter labextension develop . — Registers the Extension. Do this once as a setup step.
- What it does: Tells JupyterLab where to find your extension
- Output: Creates symlinks so changes are reflected
- Note: Also installs the Python package in editable mode
- Result: Extension appears in JupyterLab
You need BOTH steps! Building prepares the code; installing registers it with JupyterLab.
Common mistake: Running only jlpm build and expecting the extension to appear. It won't show up until you also run the installation commands.
pip install -e ".[dev,test]"
jupyter labextension develop . --overwrite
jupyter server extension enable jupytercon2025_extension_workshopDevelopment with auto-rebuild (recommended):
jlpm run watch # Auto-rebuild on file changes (keep running)
# In another terminal:
jupyter labAfter editing TypeScript (files in src/):
- If using
jlpm run watch: Just refresh your browser (Cmd+R / Ctrl+R) - If not using watch: Run
jlpm build, then refresh your browser
Quick TypeScript validation (optional, for fast feedback):
npx tsc --noEmit src/index.ts # Check single fileAfter editing Python (files in jupytercon2025_extension_workshop/):
- Restart the JupyterLab server (Ctrl+C in terminal, then
jupyter labagain) - No rebuild needed!
- Only run
pip install -e .if you changed package structure (renamed package directory, or modified entry points inpyproject.toml)
Memory aid: "What did you change? Restart that!"
- Changed JavaScript → Build (or auto-builds with watch) → Refresh browser
- Changed Python → Restart JupyterLab server (no build needed)
jupyter labextension list # Check if extension is installed
jupyter server extension list # Check backend extension
jlpm run lint # Lint frontend codeBrowser console (ask user to check):
- Request user to open browser console (F12 or Cmd+Option+I)
- Ask user to report any JavaScript errors
- Ask user to check for failed network requests to backend endpoints
- Ask user if the extension appears to be loaded
Server logs (terminal running jupyter lab):
- Check for Python errors or exceptions
- Verify backend routes are registered
- Look for HTTP request logs
If your extension doesn't appear in JupyterLab after building:
1. Check if the extension is installed:
jupyter labextension listYour extension should appear as "enabled" and "OK".
2. If NOT in the list, run the installation commands:
pip install -e .
jupyter labextension develop . --overwrite
jupyter server extension enable jupytercon2025_extension_workshop3. Did you restart JupyterLab?
- Changes require a full restart (Ctrl+C in terminal, then
jupyter labagain) - Simply refreshing the browser is NOT enough for new extensions
4. Ask user to check the browser console (F12 or Cmd+Option+I):
- Request user to look for JavaScript errors that might prevent extension activation
- Ask user to search for the extension ID (
jupytercon2025_extension_workshop) to see if it loaded - Ask user to report any error messages or warnings
5. Verify the build output:
ls -la lib/ # Should contain compiled .js files
ls -la jupytercon2025_extension_workshop/labextension/ # Should contain bundled extension6. If still not working, try a clean rebuild following the reset instructions below
Common causes:
- ❌ Only ran
jlpm buildwithout installation commands - ❌ Forgot to restart JupyterLab after installation
- ❌ Running commands outside the activated environment
- ❌ Build errors that were missed (check terminal output)
jlpm clean:all # Clean build artifacts
# git clean -fdX # (Optional) Remove all ignored files including node_modules
jlpm install # Only needed if you used 'git clean -fdX'
jlpm build
pip install -e ".[dev,test]"
jupyter labextension develop . --overwrite
jupyter server extension enable jupytercon2025_extension_workshop✅ Do: Use a virtual environment (conda/mamba/micromamba/venv)
✅ Do: Use jlpm exclusively
❌ Don't: Mix package managers (npm, yarn) with jlpm
❌ Don't: Mix lockfiles — keep only yarn.lock, not package-lock.json
✅ Do: Follow the template structure
- Keep configuration files in project root:
package.json,pyproject.toml,tsconfig.json - Backend routes:
jupytercon2025_extension_workshop/routes.py - Server extension config:
jupyter-config/server-config/jupytercon2025_extension_workshop.json - Frontend code:
src/index.tsand othersrc/files - Styles:
style/index.css
❌ Don't: Rename or move core files without updating all references in configuration
✅ Do: Update version in package.json only
- The
package.jsonversion is the source of truth pyproject.tomlautomatically syncs frompackage.jsonviahatch-nodejs-version- Follow semantic versioning: MAJOR.MINOR.PATCH
❌ Don't: Manually edit version in pyproject.toml — it's dynamically sourced from package.json
Note: Releases are handled by GitHub Actions, not manually. AI agents should only update versions when explicitly requested by the user.
✅ Do: Start simple and iterate
- Begin with minimal functionality (e.g., a single command or widget)
- When integrating backend/frontend: See Integration Workflow for the correct order
- Add backend routes or verbs only when frontend needs them
- Test in running JupyterLab frequently
- Ask user to check browser console and review terminal logs for errors
❌ Don't: Build complex features without incremental testing
❌ Don't: Write frontend interfaces without first checking the backend API contract in jupytercon2025_extension_workshop/routes.py
✅ Do: Use jlpm consistently
jlpm install
jlpm build❌ Don't: Mix package managers or lockfiles
- Don't use
package-lock.json(this project usesyarn.lock) - Don't run
npm install
✅ Do: Use relative imports in TypeScript (src/ files)
import { MyWidget } from './widgets/MyWidget';❌ Don't: Use absolute paths or assume specific directory structures
✅ Do: Wrap async operations in try-catch (in src/api.ts, widget code)
try {
const data = await fetchData();
} catch (err) {
showErrorMessage('Failed to fetch data');
}❌ Don't: Let errors propagate silently or crash the extension
✅ Do: Namespace all CSS in style/index.css
.jp-jupytercon2025-extension-workshop-widget {
padding: 8px;
}❌ Don't: Use generic class names like .widget or .button
✅ Do: Dispose resources in widget dispose() methods
dispose(): void {
this._signal.disconnect();
super.dispose();
}❌ Don't: Leave event listeners or signal connections active after disposal
✅ Do: Use relative imports within your package
from .routes import setup_route_handlers❌ Don't: Use absolute imports like from jupytercon2025_extension_workshop.routes import ...
Use these patterns consistently throughout your code:
- Plugin ID (in
src/index.ts):'jupytercon2025_extension_workshop:plugin' - Command IDs (in
src/commands.tsorsrc/index.ts):'jupytercon2025_extension_workshop:command-name'- For multiple commands, create
src/commands.tswith a centralizedCOMMANDSmapping - For 1-2 commands, define directly in
src/index.ts
- For multiple commands, create
- CSS classes (in
style/index.css):.jp-jupytercon2025-extension-workshop-ClassName - API routes (in
jupytercon2025_extension_workshop/routes.py):url_path_join(base_url, "jupytercon2025_extension_workshop", "endpoint")
See Development Workflow section for full command reference.