Python Language Levels (PLL)
A beginner-friendly Python extension for VS Code, powered by Pyodide.
It works in desktop VS Code and in vscode.dev (web).
Features (MVP)
- Integrated interactions view. A single PLL panel webview
shows banners, your typed input, stdout/stderr, results, structured
errors, and images all in one scroll-back-able stream. The input row at
the bottom accepts Python expressions and statements. Multi-line input
uses Shift+Enter; otherwise Enter submits, with automatic continuation
prompts when
codeop.compile_command reports the input is incomplete
(matching CPython's interactive shell). Up/Down arrows scroll through
history. Ctrl/Cmd+L clears the stream. Works identically in desktop and
vscode.dev.
- Run Python files into the same session.
Python Language Levels: Run Active File (also on the editor title run button for .py files) clears the
stream, runs the file in the same Python globals the REPL uses, and
leaves you at a fresh prompt with all of the file's definitions
available.
- Beginner-friendly errors. Currently
NameError is rewritten to a
plain-language explanation with a "what / why / how to fix" breakdown,
rendered as a structured block in the interactions view and as a VS
Code diagnostic on the offending line/identifier. The location text is
clickable and jumps to the source line.
- Language levels (
#beginner / #intermediate / #advanced). The
first non-blank line of a file may be a magic comment that selects a
language level. After a Run File, the active level is shown in the
interactions header (e.g. hello.py [beginner]).
#beginner (strictest): flags variable shadowing (including
built-ins), variable reassignment in any scope, and the
global / nonlocal keywords. If any check fires the file isn't
executed and findings are surfaced in the interactions view and the
editor.
#intermediate: same shadowing and global / nonlocal rules, but
reassignment is only flagged at module scope. This means rebinding
inside a function body is fine, so for-loop accumulator patterns
(total = 0; for x in xs: total = total + x) work.
#advanced (the default if no header is present) disables all static
checks and runs the file as plain Python. The REPL itself is always
advanced.
- Images (HtDP/Pyret-style). A small image library (
circle, square,
rectangle, triangle, ellipse, regular_polygon, star, text,
beside, above, overlay, rotate, scale, flip_*, ...) is
available without import at every level. Top-level expressions in a file
that evaluate to images auto-display inline in the interactions
stream, interleaved with text output. The same happens for image
values returned from REPL evaluations. Images are rendered as SVG (so
they scale cleanly) and each one has a Save SVG button.
- Tables and charts. A Pyret-style
Table type lives next to the
image library (also no import). Tables are immutable, methods chain,
and every operation returns a new table. The interactions view
renders tables as inline cards (sticky header, zebra rows, Save CSV),
and chart methods (bar_chart, scatter_chart, line_chart,
histogram) produce SVG images that flow into the same image-card
pipeline. See samples/tables.py for a tour. Need real pandas?
t.to_pandas() is an escape hatch.
Architecture
src/
├── extension.ts Desktop entrypoint (Node host)
├── web/extension.ts Web entrypoint (vscode.dev)
├── web/pyodideWorker.ts WebWorker that hosts Pyodide in the browser
├── web/pyodideRuntime.ts Talks to the worker
├── desktop/pyodideRuntime.ts Loads Pyodide directly in the Node host
└── common/
├── commands.ts Run File / Show Interactions / Clear commands
├── replSession.ts Drives the interactions view: init,
│ REPL multi-line buffer, file runs, exec chain
├── interactionsView.ts WebviewView provider for the integrated
│ text + image stream + input row
├── level.ts #beginner / #intermediate / #advanced header parser
├── errorFormatter.ts Plain-text rendering for diagnostic tooltips
├── diagnostics.ts VS Code DiagnosticCollection (multi-finding)
├── pyodideRunner.ts Bootstrap loader + types
├── deliverResult.ts Translates Python results to ExecutionEvents
├── pyodideBootstrap.py Real Python: run / repl-eval / static analyzer
├── imageLib.py Real Python: SVG image primitives + combinators
├── tableLib.py Real Python: Pyret-style Table + charts
├── analyzers/
│ ├── types.ts AnalysisFinding, RuntimeAnalyzer
│ ├── nameErrorAnalyzer.ts Runtime: NameError -> friendly finding
│ ├── registry.ts Runtime analyzer registry
│ └── static/
│ ├── shadowingExplainer.ts shadowing + shadowing-builtin
│ ├── reassignmentExplainer.ts reassignment
│ ├── disallowedKeywordExplainer.ts `global` / `nonlocal`
│ └── registry.ts Wraps Python-side raw findings
└── errors/
├── pythonErrorParser.ts
└── nameErrorExplainer.ts
media/
├── interactionsView/
│ ├── style.css Stream + input row styling
│ └── main.js View-side state, history, message routing
├── pll-icon.svg Panel container icon
└── error-gutter.svg Diagnostic gutter icon
The static analyzer itself (scope builder, shadowing/reassignment checks)
lives in pyodideBootstrap.py. esbuild's text loader inlines that file as
a string at build time so it's loaded into Pyodide once on init - which means
the analysis runs in the same Python interpreter that runs the user's code,
in both desktop and web hosts.
Images
PLL ships a small SVG-based image library inspired by Racket's
2htdp/image and Pyret's image-lib. The primitives are auto-imported into
user globals at every level, so a beginner can write:
#beginner
circle(50, "solid", "red")
beside(
triangle(60, "solid", "gold"),
square(60, "outline", "navy"),
)
above(
rectangle(120, 40, "solid", "black"),
rectangle(120, 40, "solid", "red"),
rectangle(120, 40, "solid", "gold"),
)
Top-level expressions auto-display inline in the interactions
view, in the same stream as text output, so the order of your prints
and your images is preserved exactly. In the REPL, evaluating an
expression that returns an Image shows it the same way. Each image card
has a "Save SVG" button.
Available primitives: circle, square, rectangle, ellipse,
triangle, right_triangle, regular_polygon, star, star_polygon,
line, text. Combinators: beside, above, overlay, underlay,
rotate, scale, flip_horizontal, flip_vertical. Inspection:
image_width, image_height, empty_image. Colors are CSS strings
("red", "#ff0000") or (r, g, b) / (r, g, b, a) tuples.
Smoke tests
pnpm run smoke # static analyzer + explainers + image library/runtime
Development
pnpm install
pnpm run build # one-shot build (also copies Pyodide assets into vendor/)
pnpm run watch # rebuild on change
pnpm run vsce:package # produce a .vsix (runs vscode:prepublish first)
Running the desktop extension
Open this folder in VS Code and press F5 -> Run Extension (Desktop).
A second window opens with the extension loaded.
Running the web extension (the primary target)
There are three options, in increasing order of "how realistic":
F5 -> Run Extension (Web) in desktop VS Code. Uses
extensionDevelopmentKind=web so the extension host runs the web
bundle (dist/web/extension.js) and a real Worker. This is the
fastest iteration loop because it gives you the full debugger.
pnpm run test-web spins up a local copy of vscode-web (the
exact build behind vscode.dev) and opens it in Chromium with our
extension preloaded. Closest thing to vscode.dev short of actually
publishing.
pnpm run test-web # ensures Chromium is downloaded, builds,
# then opens vscode-web in Chromium
pnpm run test-web:server # build + run server on :3000 only
# (browse to it from any browser)
pnpm run setup:browser # one-time: download Playwright Chromium
# (chained from test-web; safe to run alone)
The script enables --coi (cross-origin isolation) so Pyodide's
workers/SharedArrayBuffer features work, and points the workspace at
samples/ so you can immediately open hello.py or name_error.py.
First run downloads vscode-web into .vscode-test-web/ (~30 MB) and
Playwright Chromium into ~/Library/Caches/ms-playwright/ (~150 MB);
both are cached for subsequent runs.
Iteration tip: in one terminal run pnpm run watch so esbuild
rebuilds on save; in another run pnpm run test-web:server and just
reload the browser tab to pick up changes.
package.json enables Playwright's postinstall via
pnpm.onlyBuiltDependencies so a fresh pnpm install fetches
Chromium for you. If you ever skipped it (e.g. cloned with
--ignore-scripts), pnpm run setup:browser re-runs that step.
Real vscode.dev with the published or sideloaded extension.
This is what end users hit; only useful once you're ready to publish.
Configuration
pll.pyodideIndexUrl - base URL for Pyodide assets (web only).
Defaults to the matching pinned CDN build.
Beginner-friendly editor lockdown
This extension ships opinionated configurationDefaults that quiet down
the default Python editing experience so beginners only see what we
explicitly turn on. None of these touch your settings.json; they are
defaults the extension contributes, so any setting you change yourself
still wins.
What stays on by design:
- Syntax highlighting (built-in TextMate grammar)
- Line numbers, bracket matching, indent guides
- Auto-closing brackets / quotes (helpful for newcomers)
- The Problems panel (so our friendly errors show up)
- The PLL interactions view and
Run Active File button
What we turn off for [python] files:
- All autocomplete popups:
editor.quickSuggestions,
suggestOnTriggerCharacters, tabCompletion, wordBasedSuggestions,
parameterHints, snippetSuggestions, suggest.showWords/showSnippets
- Inline AI suggestions:
editor.inlineSuggest.enabled,
github.copilot.enable.python, github.copilot.editor.enableAutoCompletions,
cursor.cpp.disabledLanguages (Cursor Tab) - best-effort across forks
- CodeLens, lightbulb (quick-fix), minimap, sticky scroll, linked editing
- Format on save / paste / type
What we turn off globally (no-ops if the extension isn't installed):
- Microsoft Python extension legacy linting:
python.linting.{enabled,pylintEnabled,flake8Enabled,mypyEnabled,banditEnabled,pycodestyleEnabled,pydocstyleEnabled}
- Pylance:
python.languageServer = None,
python.analysis.{autoImportCompletions,typeCheckingMode,completeFunctionParens,indexing,useLibraryCodeForTypes,diagnosticMode}
- Standalone linters/formatters - we silence them via the python-tools
template's
ignorePatterns: ["**"] (which matches every absolute path):
pylint.{enabled,ignorePatterns}, flake8.{enabled,ignorePatterns},
bandit.{enabled,ignorePatterns},
mypy-type-checker.{enabled,ignorePatterns,reportingScope},
ruff.{enable,lint.enable,ignorePatterns}
matangover.mypy (no settings-based off switch) -
mypy.runUsingActiveInterpreter, mypy.checkNotebookFiles,
mypy.checkAllOpenFolders, mypy.targets: []. This still triggers on
save for the active file - see "Extension guard" below.
- Pyright family:
pyright.disableLanguageServices +
pyright.disableOrganizeImports, plus the same pair for basedpyright
- Formatters:
black-formatter.formatOnSave, isort.formatOnSave
- Other noise:
python.terminal.activateEnvironment,
python.experiments.enabled, python.showStartPage,
breadcrumbs.enabled
Extension guard (auto-prompt on activation)
Some Python extensions emit diagnostics regardless of settings - the most
notorious is matangover.mypy, which has no enabled and no
ignorePatterns setting; the only way to silence it is to disable the
extension itself.
To handle this, the
extension guard
runs on activation: it scans installed extensions, lists known
beginner-conflicting ones (matangover.mypy,
ms-python.{mypy-type-checker,pylint,flake8,bandit},
ms-pyright.pyright, detachhead.basedpyright, charliermarsh.ruff),
and shows a single warning notification with two buttons:
- Show & Disable opens each conflicting extension's details page so
you can click Disable (Workspace) on each, then offers to reload.
- Don't ask again records dismissal in the workspace state so the
prompt won't reappear in this workspace.
Things you may still need to disable manually
Some features are owned by other tools that ignore both VS Code's
configurationDefaults and our extension-detector. If they appear in
your editor and you want a fully clean beginner experience, disable them
by hand:
- Cursor's "Tab" autocomplete: in addition to the
cursor.cpp.disabledLanguages hint above, you may need to toggle
Cursor's AI features in Cursor Settings -> Features -> Tab.
- Any other AI assistant extension (Codeium, Tabnine, Supermaven, ...) -
disable per-language or per-workspace in that extension's own settings.
- Workspace-installed extensions you don't want active here: use
"Extensions: Disable (Workspace)" from the command palette.
If a setting we ship isn't aggressive enough for your classroom, override
it in your workspace .vscode/settings.json - your value always wins
over our defaults.
| |