Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>Python Language LevelsNew to Visual Studio Code? Get it now.
Python Language Levels

Python Language Levels

Python Language Levels

|
111 installs
| (0) | Free
Beginner-friendly Python in the editor: an integrated REPL with inline images, tables and animations, language levels, and friendly errors. Powered by Pyodide. Works in desktop VS Code and vscode.dev.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Python Language Levels (PLL)

PLL is a VS Code extension for learning Python. It runs your programs inside the editor and shows the results in an interactions panel — output, errors, images, tables, and a prompt where you can try extra Python after a run.

It works in vscode.dev (in the browser) and in desktop VS Code. You do not need to install Python on your computer.

Install

PLL needs VS Code 1.101 (June 2025) or newer. If you are installing VS Code now, you have it; if yours is older, update it first.

  1. Open VS Code (desktop or vscode.dev).
  2. Open the Extensions view (the four squares in the left sidebar).
  3. Search for Python Language Levels.
  4. Click Install. If you are asked to trust the publisher, do that.

You do not need Microsoft's Python extension. If VS Code offers to install it, you can skip it. PLL is enough.

Run a program

  1. Create a file whose name ends in .py, for example hello.py.

  2. Type a small program:

    print("hello")
    
  3. Look at the top right of the editor, on the same bar as the file name. Click PLL: Run Python File.

    If you do not see that text, open the Command Palette (Ctrl+Shift+P on Windows, Cmd+Shift+P on a Mac) and run PLL: Run Python File.

The first run can take a little while: PLL is starting Python in the editor. Later runs are faster.

Your file stays a normal editor. Results appear in the PLL panel at the bottom of the window (near Problems and Terminal). If that panel is hidden, run PLL: Show Interactions from the Command Palette.

Try things after a run

The interactions panel has a prompt at the bottom. After a file has run, you can type extra Python there and press Enter. Names you defined in the file are still available. The prompt uses the same language level as that run (the one shown in the header).

  • Enter runs what you typed.
  • Shift+Enter adds another line (for a longer snippet).
  • If Python is waiting for more input (for example after if True:), PLL will keep prompting until the snippet is complete.
  • Up and Down move through things you typed earlier.
  • Ctrl+L (Windows) or Cmd+L (Mac) clears the interactions panel. You can also run PLL: Clear Interactions.

Stopping a program

If a program runs longer than you expect - a loop that never ends, say - click Stop in the interactions panel. Ctrl+C (with nothing selected) and PLL: Stop Program in the Command Palette do the same thing. The program stops with a KeyboardInterrupt, and anything it printed first is kept.

Stop works the same way while your tests run. The test that was running is marked as stopped, and nothing after it runs - not the rest of the tests, and not the program.

This works for ordinary Python code. If your program is stuck inside a library (a very long pandas operation, for example), or if it catches KeyboardInterrupt itself, PLL will tell you it could not stop it - reload the window in that case.

Language levels

The first non-blank line of a file chooses how strict PLL is. After you run the file, the current level is shown in the interactions header (for example hello.py [beginner]).

#level beginner

Write it exactly like that, in lower case, as the first line that is not blank.

Line in your file What it does
#level raw Nothing is checked. Your program runs exactly as plain Python would, with PLL's built-in libraries (images, tables) and the interactions panel still available. This is what you get if you leave the line out.
#level beginner Strictest. PLL warns about reassigning a variable, reusing a name that hides another name (a built-in like list, or one of PLL's own like circle), and the global / nonlocal keywords. If it finds a problem, it does not run the file. Type annotations are checked as the program runs.
#level intermediate Same rules about hiding names and global / nonlocal, but you may reassign variables inside a function. That is useful for introducing for loops, where you need mutable accumulators. Reassigning at the top of the file is still flagged. Annotations are checked.
#level advanced No extra checks before the file runs. Annotations are still checked as the program runs, but by Python's own rules (so True counts as 1).

The level is the only thing that decides what gets checked — there is no separate setting to keep in sync with it.

If your course sets pll.newFileLevel, every new .py file you create starts with that level line already written in, so you do not have to remember it. You can always change or delete the line.

The prompt at the bottom of the interactions panel uses the same level as the last run (the one shown in the header). If you have not run the file yet, the prompt is raw.

Type annotations are checked as your program runs

If you write annotations, PLL checks them while the program runs and stops with an explanation the moment a value does not match:

def book_cost(num_books: int, hardcover: bool) -> float:
    if hardcover:
        return num_books * 25
    return num_books * 12

book_cost("three", True)

book_cost expects num_books to be a whole number (int), but got a string (str).

You do not need to import anything, and it works at every level except #level raw. What is checked:

  • the arguments you pass to a function, checked at the call
  • the value a function returns, including a function that ends without returning anything when it says it returns something
  • variables you annotate, like total: int = 0
  • every item in an annotated list, dict, set, or tuple

Functions without annotations are left completely alone. A whole number is accepted wherever a float is expected, as it is in normal Python.

At #level beginner and #level intermediate, True and False are not accepted where int or float is annotated:

#level beginner

shelf_count: int = True   # error: True is not a whole number here

Python itself counts True as 1, so this is a rule PLL adds rather than one Python enforces — a value that is really a yes/no answer should be annotated bool. At #level advanced this follows Python's own rule and is allowed.

To turn annotation checking off entirely, write #level raw at the top of the file (or leave the level line out).

Tests

You can put tests in the same file as the code they check. A test is a function whose name starts with test_. Use assert to check that something is true:

def add(x, y):
    return x + y

def test_add():
    assert add(2, 3) == 5

When you click PLL: Run Python File, PLL runs the tests first and shows a pass/fail card in the interactions panel. If a test fails, you can click it to jump to that test. After the tests, PLL still runs the rest of the file so you can use your functions at the prompt.

You do not need a separate test file, and you do not need to run pytest in a terminal.

For decimal (floating-point) numbers, exact == can be unreliable. Import pytest and use pytest.approx:

import pytest

def test_cost():
    assert 0.1 + 0.2 == pytest.approx(0.3)

Checking your tests against your course's code

Some assignments ask you to write the tests first, before the code. There is nothing of your own to run them against yet — so PLL can run them against code your course wrote: implementations known to be correct, and implementations known to contain a bug.

Your course gives you a line to put at the top of the file:

#examplar https://example.edu/hw3.json

After that, every run adds a card for each function the assignment asks for:

total                                                        Examplar
  Against correct implementations: all 3 of your tests pass.
  Against buggy implementations: caught 4 of 6 - missed 2, 5.

The first line asks whether your tests are right — do they agree with code that works? The second asks whether they are thorough — how many of the deliberately broken versions did they notice? One test_ function is enough to start; you do not need to have written any of the assignment.

It will not tell you the answer. If a test expects the wrong thing you are told which test, never what it should have said — otherwise you could read the assignment straight off the card, one deliberately-wrong test at a time. A buggy version you missed gives up its number and nothing else. Working out what you failed to check is the exercise.

Coverage only appears once every test of that function passes, because a wrong test fails on the buggy versions too and would look as though it had caught them. Each function is scored on its own, and one you have not started says simply "No tests yet."

Two things to know. Your program does not run during the check — only your definitions are loaded, so a print at the end of your file still happens once, right afterwards. And files next to your program are not available during it, so a test that opens data.csv cannot run there; the card says so rather than calling that test wrong.

None of this is a grade. It shows you where your tests are thin while you still have time to do something about it.

Images

You can make pictures with built-in functions. You do not need to import anything. If a line in your file produces an image, PLL shows it in the interactions panel, in order with any print output.

#level beginner

circle(50, "solid", "red")

beside(
    triangle(60, "solid", "gold"),
    square(60, "outline", "navy"),
)

Each picture has a Save SVG button if you want to keep it.

Shapes: circle, square, rectangle, ellipse, triangle, right_triangle, regular_polygon, star, star_polygon, line, text.

Combining and transforming: beside, above, overlay, underlay, rotate, scale, flip_horizontal, flip_vertical.

Placing things exactly: overlay_xy and underlay_xy move the second image by an offset — overlay_xy(a, 20, 10, b) puts b 20 to the right and 10 down from a. Negative offsets move it left or up, and the picture grows that way rather than cutting anything off.

Choosing which edges line up: beside_align("top", ...), above_align("left", ...), overlay_align("right", "bottom", ...), and underlay_align. Horizontal positions are "left", "center", "right"; vertical are "top", "center", "bottom".

Scenes: empty_scene(width, height) is a fixed-size canvas, and place_image(image, x, y, scene) puts an image's center at that point on it, cropping anything past the edge. crop(x, y, width, height, image) takes a piece out of an image, and frame(image) outlines its edges.

Size: image_width, image_height, empty_image.

Colors can be names ("red"), hex ("#ff0000"), or tuples (red, green, blue) with values from 0 to 255. Names are checked against the CSS colours, which are the names a browser understands, so a misspelling is an error that suggests what you meant ("rd" → "Did you mean red?") rather than an invisible shape. "transparent" works too.

Loading a picture

load_image reads a picture from a file next to your program or from an address, working out which from what you give it:

cat = load_image("cat.png")
cat = load_image("https://example.edu/cat.png")

It gives you an ordinary picture, so everything above works on it — scale, rotate, beside, place_image and the rest. PNG, JPEG, GIF, WebP and SVG files are understood, up to 2 MB each — these are for the graphics a program draws with, not for photographs.

Tables and charts

PLL also includes a table type (again, no import). Tables do not change in place: each operation returns a new table.

people = table(
    ["name", "age"],
    [
        ["Ada", 36],
        ["Grace", 85],
    ],
)

people
people.bar_chart("name", "age", title="Age")

Tables show up as a card you can scroll, with a Save CSV button. Charts show up as images.

Loading a CSV

load_table reads a CSV, either from a file next to your program or from an address — it works out which from what you give it:

cars = load_table("cars.csv")
cars = load_table("https://example.edu/cars.csv")

The first row names the columns, and every value arrives as text — including the ones that look like numbers. An empty cell is "". Convert a column when you want to chart it or average it:

cars = load_table("cars.csv").transform_column("mpg", float)
cars.histogram("mpg")

That is one more line than guessing which columns are numbers, and it is the line that says what you meant: a column of years or postcodes is not something to do arithmetic on, and one stray n/a would otherwise change what the whole column holds.

Methods

Useful ones include filter, transform_column, add_column, order_by, select_columns, head, columns, length, row, column, sum, mean, min, and max. For a median, a standard deviation or anything else of that sort, use a column with Python's own statistics module.

Two tables are == when they have the same columns, in the same order, holding the same values — so you can test a function that builds a table by comparing it with the table you expect.

Charts

Chart What it shows
bar_chart(labels, values) one bar per row
freq_bar_chart(column) one bar per distinct value, counting the rows
pie_chart(labels, values) each row's share of the total
scatter_plot(x, y) one point per row
line_chart(x, y) points joined in order of x
dot_plot(column) one dot per row, stacked where rows share a value
box_plot(column) quartiles, whiskers and outliers
histogram(column) counts per bucket — bins= how many, or bin_width= how wide
lr_plot(x, y) a scatter plot with the line of best fit, and r² in the title

labeled_scatter_plot, labeled_dot_plot and labeled_lr_plot take an extra first argument: a column to colour the points by, with a key. Every chart takes an optional title=. linear_regression(x, y) gives you the slope, intercept and r² as numbers instead of a picture.

To draw a function rather than a table, function_plot takes the function and the range to draw it over:

function_plot(lambda x: x * x, -3, 3)

If you already know pandas, my_table.to_pandas() gives you a DataFrame. You can also import pandas as pd and read a CSV from a URL with pd.read_csv("https://..."). That works in desktop VS Code and in the browser. In the browser, the site must allow cross-origin requests (CORS) — the same goes for load_table with an address.

Animations and interactive programs

A reactor is an interactive program: a starting state, a function that draws it, and functions that change it when something happens. It shows up as a card right in the interactions panel.

The quickest way in is animate, where the state is just a frame counter:

scene = empty_scene(320, 140)

animate(lambda n: place_image(circle(14, "solid", "crimson"), (n * 4) % 320, 70, scene))

The long form names each handler:

reactor(
    init=(160, 70),
    to_draw=lambda spot: place_image(star(18, "solid", "gold"), spot[0], spot[1], scene),
    on_key=move,          # move(state, key) -> new state
    title="Arrow keys",
).interact()
Handler Called with When
to_draw (state) every frame; must return an image
on_tick (state) on the clock
on_key (state, key) a key press — "left", "a", " ", …
on_mouse (state, x, y, event) "button-down", "button-up", "drag", "move", "enter", "leave"
stop_when (state) after each change; True ends it
on_receive (state, message) a message from a server (see below)

Also tick_rate (seconds between ticks, default about 1/28) and title. Each handler returns the new state. Click the picture before using the keyboard, so the keys go to the reactor and not the prompt.

Playing, pausing, and rewinding

The card has play / pause, a single-step button, and a slider. Every state the reactor passes through is recorded, so you can drag the slider back to watch what happened and then play forward again from there.

big_bang(init, ...) is the same as reactor(...).interact().

Testing a reactor without watching it

A reactor is a value, so you can run it without any of the animation:

countdown = reactor(init=10, to_draw=..., on_tick=lambda n: n - 1,
                    stop_when=lambda n: n <= 0)

countdown.simulate_trace(20).get_trace()   # [10, 9, 8, ..., 0]
countdown.get_value()                      # 10 — the original is unchanged
countdown.react({"kind": "tick"}).get_value()   # 9

react returns a new reactor, so this works in tests and at the prompt.

Talking to a universe server

A reactor with a register address is a world: it connects to a server and can send and receive messages.

reactor(
    init=...,
    to_draw=...,
    on_key=lambda state, key: package(new_state, {"at": [x, y]}),
    on_receive=lambda state, message: ...,
    register="ws://localhost:8080",
).interact()

package(state, message) returns the new state and sends a message. Whatever the server sends back arrives at on_receive. The card shows whether it is connected.

You write worlds; the server is a separate program your course runs, in whatever language they like. Messages are JSON, one value per message, in each direction — so a conforming server is small, and your course will give you one to run.

In the browser, a page served over https (including vscode.dev) can only reach a wss:// address — except on localhost, which is allowed either way.

Interactive input()

input() asks for a line in the interactions panel, in desktop VS Code and in the browser. The prompt string prints first, then you type a reply and press Enter. Ctrl+C (with nothing selected) cancels and the program gets an EOFError.

name = input("What is your name? ")
print("Hello,", name)

Files next to your program

open("data.csv") and pd.read_csv("data.csv") read files that sit in the same folder as the .py file you ran. That works in desktop VS Code and in the browser (including vscode.dev). After the program finishes, files it wrote or changed — for example to_csv("out.csv") or open("out.csv", "w") — show up in that folder. You can open them in the editor. PLL does not overwrite your .py files.

Untitled editors (not yet saved to a folder) have no sibling files to load or save.

Friendlier errors

If you use a name that is not defined, PLL rewrites Python's NameError into a short explanation of what went wrong and how to fix it. The message appears in the interactions panel and as a mark in the editor. Click the location in the message to jump to that line.

Commands

All of these are available from the Command Palette. PLL: Run Python File also appears in the editor title bar when a .py file is open.

Command What it does
PLL: Run Python File Run tests (if any), then run the file.
PLL: Show Interactions Open the interactions panel.
PLL: Start REPL Open the interactions panel (same as Show Interactions).
PLL: Stop Program Stop the program that is running.
PLL: Clear Interactions Clear the panel.

Copy and paste

Ctrl/Cmd+C, X, and V work as usual, both in the editor and in the interactions panel, in the browser and on the desktop. Right-click also works.

If the shortcuts do nothing in the browser and you use a layout other than QWERTY (Dvorak, Colemak, …), add this to your user settings (Command Palette → Preferences: Open User Settings (JSON)) — a .vscode/settings.json in the folder will not work:

{
  "keyboard.dispatch": "keyCode"
}

Otherwise, check whether something in your own Keyboard Shortcuts has taken over Ctrl/Cmd+C or V. PLL: Editor Copy/Cut/Paste in the Command Palette also work as a fallback.

A quieter editor

PLL turns off many extra Python tools (autocomplete popups, extra linters, and similar) so the editor stays simple while you are learning. Settings you choose yourself still win over PLL's defaults.

If another Python extension is installed and might add confusing messages, PLL may ask whether to disable it for this workspace. That does not uninstall the extension.

Running programs without the editor

The same language levels are available as a command-line tool, so a program behaves the same on a terminal as it does in the panel:

npx pll-python hw.py

The level still comes from the file's own #level line, tests still run first, and errors are worded the same way. Pictures cannot be drawn in a terminal (each prints a note, or use --save-images) and reactors do not animate, but tables print as text and everything else is the same code.

Exit codes make it usable for marking: 0 ran and tests passed, 1 the program raised or was stopped, 2 level checks blocked it, 3 a test failed.

Course staff can also build Examplar bundles with it. See pll-python on npm for that and the full list of options.

For course staff and contributors

How PLL is built, how to run it from source, how to author Examplar bundles, and how the editor defaults work are documented in the repository.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft