Prepare the task

  1. Create a class in the “Council” format, or pick “Council” from the lock menu on a single cell. The lock appears when the “Edit cells” or “Run code” rule is reserved for the teacher, as in the “Lecture” format. In a room where everyone can both edit and run code, there is no lock.
  2. In shared cells, prepare the data, the imports and the task statement.
  3. Run the setup code so the values you need are in the kernel.
  4. Open the task cell: each participant gets their own sheet.

The Council format differs from Lecture in what opening a cell does. All other permissions can be set separately.

Students answer separately

Answers are stored on the server, separately from the shared document. A student writes their own version (up to 8,000 characters) and presses “Submit” or ⌘ / Ctrl + Shift + Enter. “Edit” takes a submitted answer back into work, and “Restore” resets the sheet to the original task. Until the teacher shows an answer, each participant sees only their own work, while the teacher sees both submitted answers and drafts. If an attempt fails with an error, the student can press “Ask the oracle”: only they and the teacher see the reply, and the question counts toward the oracle limit.

Who runs attempts is set in the console's rules: Only me (the default), Everyone in turn or On request. The room's “Run code” rule does not by itself let anyone run Council attempts. In every mode, attempts run one after another in the notebook's kernel — except in a council on machines, where they run at the same time, each in a worker of its own.

Council rules

Council rules are set for each cell in the console window. Its header sums them up in one line, for example “Runs by me only · each run capped at 30 s · on screen with names”. To change a rule, press Rules, any value in that line, or the G key. Changes take effect at once; there is no Save button.

RuleOptionsWhat it means
Who runs code“Only me” (default), “Everyone in turn”, “On request”Students write the code and you run it; a student runs their own attempt; a student asks and you approve that exact version of the code. Changing the mode drops pending requests.
Run limit5 s, 15 s, 30 s (default), 1 min, 5 min, “No limit”If an attempt runs longer, the server interrupts it and the queue moves on. The limit applies to your own runs too. A new limit takes effect at once for waiting attempts and for the one that is running, counted from when it started. Below the options you can see how long runs of this cell usually take.
Rerun pause“Right away” (default), 15 s, 30 s, 1 min, 2 minHow long a student waits after their run finishes before they can run again or ask for a run. The pause does not apply to the teacher. If only the teacher runs code, this rule has no effect.
On the class screen“With names” (default), “Without names”Without names, a shown solution is labeled with a number, such as “Answer 12”, the same way in everyone's notebook and on the projector.

When the limit stops a run, its output shows “Stopped: the run took longer than …”, and the console marks it “Stopped: longer than …”. An interrupt cannot stop every kind of code. If an attempt in the notebook's kernel does not stop, the console says “not stopping — interrupt it or restart the kernel”; in a council on machines, the worker of such an attempt restarts by itself after the second interrupt. During the pause, the student sees a countdown, “Run in …”, instead of the button.

A council on machines has two more rows in its rules: Attempts run — Apart, in parallel (the default) or In the shared kernel, in turn, and State for attempts — Follows the kernel (the default) or On Refresh only.

Running on request

The student presses Run (or Shift + Enter) on their sheet; in this mode, that sends a request to the teacher. While the request waits, the button is replaced by “In queue” with a “cancel” link; if it is declined, the student sees “The teacher did not run it. You can press again.” A student can run or request a run only before submitting: a submitted answer has no run button, and it comes back after “Edit”. Editing the text withdraws the earlier request, so the new version needs a new one.

In the console window, requests are collected on the Queue tab under “Waiting for your approval”. The link “N awaiting approval →” appears next to the tabs, and in the work list such work is marked “Run requested”. Check the code and press Approve or Decline; Approve all approves every pending request at once. Approval puts the version you checked into the shared queue (in a council on machines, into the pool's queue); no code runs before approval. A request does not count as submitting the answer. Changing who runs code or closing the Council drops the requests.

Discuss the answers with the group

You review work in a separate console window. Under the Council cell, the teacher sees only a counter (“N submitted out of M”, drafts) and the Console button. Do not put this window on the projector: it shows names, drafts and marks. If the browser did not open the window, allow pop-ups for this page. On the Work tab, the list of submissions is on the left: newest first, with three or more identical answers folded into a group. The selected work opens on the right. You can run it, mark it Correct or Revise, write to its author, and press Show the class. The code then appears under the cell for everyone as a labeled block, and the cell's own text does not change. Clear the screen takes it down. The author sees your mark (“✓ Correct” or “✗ Has an error”) and your messages under their sheet. The “Class Oracle” tab lets you ask the oracle about the class — the task, who is stuck, the solutions themselves — if the oracle is set up.

Keys in the console window: J / K or ↓ / ↑ move to the next and previous work, Space expands a group, ⌘ / Ctrl + F searches by name, Enter shows the work to the class, R runs it, 1 marks it “Correct”, 2 marks it “Revise”, 3 clears the screen, ← / → move to the neighboring work in the same group, G opens the rules, and ? lists all keys.

What stays shared

Personal copies are made of pandas frames and series, numpy arrays, lists, dicts, sets and tuples, PyTorch tensors and models together with their optimisers, SciPy sparse matrices, scikit-learn estimators and objects of classes defined in the notebook itself. The same entry also closes the three ways one attempt could end the class for everybody: exit() and quit() are refused (in a Jupyter kernel they shut the process down and everyone loses their variables), os._exit() and os.abort() are refused too (they end the kernel process on the spot, with no traceback at all, and the whole room's variables go with it), and an attempt's memory is capped at the container's limit, so np.ones((40000, 40000)) fails with MemoryError on its own card instead of taking the notebook's kernel with it. These refusals are a speed bump, not a sandbox: ctypes, signal.raise_signal and reloading os through importlib still reach the process. A child process after fork() keeps the real os._exit, so multiprocessing and joblib work as before. Process state goes back too: random seeds, sys.stdout, sys.path, os.environ, builtins, warning filters, numpy and pandas print options, matplotlib figures.

Files on disk, modules the attempt imported, objects of types that cannot be copied (open files, generators, connections) and objects above the copy budget stay shared. Threads an attempt leaves running cannot be stopped — it is told how many in its own output. An attempt is told about the last of these in its own output, in a line like “Variable big (1.2 GB) is too large to copy for this attempt — it works on the shared one”. When that happens, work on a copy explicitly, as before:

# df was prepared by the teacher
local_df = df.copy()
local_df = local_df.dropna()
print(local_df.shape)

A council on machines shares less. Class files are read-only to an attempt, and it writes its own into a folder of its own; what cannot be copied is not in the snapshot at all, and attempts that read it run in the notebook's kernel; a worker whose attempt left threads or processes running is restarted. Imported modules and objects above the copy budget stay shared, and only between attempts on one worker until it restarts (after 20 attempts at most).

Personal copies keep a class running, but they do not create a technical security boundary. Independent grading, tamper protection and identity checks need a separate system.

The council on machines

Usually attempts run one at a time in the notebook's kernel, in one queue with the teacher's cells: with thirty attempts of 2 seconds each the last one waits over a minute, and a long cell of the teacher's holds everyone up. When a class's Where it runs sets its Council part to On machines — as the presets Council and personal notebooks on machines, Everything on machines and Everything on a machine with a card do — attempts run apart and at the same time, in a pool of workers on a machine opened to classes. An attempt does not queue behind your cells, waits for other attempts only when every worker is busy, and the notebook's queue no longer holds them: you can run your own cells, even restart the kernel, and the council goes on. Without machines, everything works as before.

The notebook's snapshot. Workers do not see the notebook's kernel directly. The kernel takes a snapshot of its variables, the state for attempts: tables and arrays, models, functions and classes defined in the notebook, imports (by module name), the state of the random number generators, pandas and numpy options, matplotlib settings and the current folder. Each worker, an ordinary Python kernel, loads the snapshot once and then runs attempts one at a time. Large numeric pieces — table columns, arrays, model weights — sit in the snapshot as separate files named by their content: they travel to the machine once, only the changed ones on a refresh, and they sit in the machine's memory once for all the workers; the rest — strings, lists, small objects — each worker holds on its own. An attempt gets its own copy of just what it changes. When the notebook itself runs on the pool's machine, the snapshot goes nowhere. Otherwise it travels through this server at the Class files rate, and one refresh carries at most 256 MB of new data: a larger first snapshot keeps attempts in the notebook's kernel (“The notebook's state does not fit — attempts run in the shared kernel.”), and with a larger refresh attempts stay on the previous snapshot (“The new state does not fit — attempts run on the state as of 14:05.”).

When the snapshot is refreshed:

  • when you open the council cell;
  • with Refresh in the pool strip;
  • by itself, when the cell's rules say State for attempts: Follows the kernel (the default). After you run a cell past the snapshot, the next attempt first waits for a new snapshot, no longer than 5 seconds from its press. Only what changed is taken again and travels to the machine, so a small change arrives quickly, while a large one, when the notebook runs elsewhere than on the pool's machine, travels at the Class files rate. An attempt that waited its 5 seconds runs on the previous snapshot, and its card says which cells that snapshot lacks: “State as of 14:05, without cells 8–9.” So does an attempt pressed during a long cell: the snapshot is taken between your cells, never in the middle of one. If the free worker holds the previous snapshot, it restarts and loads the new one before the attempt, and the attempt waits for that too.

With On Refresh only, the cell does not refresh the snapshot itself: its attempts do not wait for a new one after your cells and run on the latest one ready. But a notebook has one snapshot for all its council cells, so it also changes with Refresh, when a council cell of the notebook is opened, and when an attempt of another open cell that follows the kernel refreshes it; and the pool takes one by itself when it is left with no snapshot it can use. Restarting the notebook's kernel does not wipe the snapshot: attempts run on the previous one until you run cells again.

What an attempt sees and changes.

  • The snapshot's variables — the ones the notebook's kernel held when the snapshot was taken, with the same state of the random numbers: identical attempts print identical numbers. An attempt changes its own variables as it likes: afterwards the worker goes back to the snapshot, and nothing returns to the notebook's kernel, so an attempt cannot spoil the teacher's variables.
  • The class files, read-only: the machine has a copy of the folder, fresh as of the attempt's start. open('data.csv') reads the class's file, while to_csv('out.csv') and savefig('fig.png') write into the attempt's own folder, so thirty attempts no longer overwrite one out.csv. A class file cannot be overwritten: Read-only file system.
  • A CPU and a memory limit of its own: Memory per attempt in Where it runs, 1 GB by default; personal copies of the data count toward the same limit. A greedy attempt gets its own MemoryError, and neither its neighbours nor the notebook notice.

What comes back. Output — text, pictures, errors — reaches the author and the console as before; Correct, Revise and Show the class work the same way. The files an attempt wrote are listed on its card with their sizes: up to 20 files, each up to 1 MB and 5 MB in all, are brought from the machine and kept with the attempt on the server, and the rest are only named. While a file is on its way from the machine, it says on its way; once here, it is downloaded from the card (↓) — by the author from their sheet, by the teacher from the console; other students and the class on the screen get no attempt's files. A file that never came — the machine was lost, the agent would not send it — is named as not kept. Only Put in cell and run still changes the shared state: it runs the solution in the notebook's kernel for real, on its live variables, and its files land in the shared folder. When the kernel has moved on since the snapshot, the console warns: “The kernel has run 3 cells since the snapshot — the result in the notebook may differ.” — in the work before Put in cell and run, and, for an attempt on an older snapshot, after the press, under the line that says the code is in the cell.

What cannot be carried over. A snapshot leaves out an open database connection, an open file, a network socket, a generator, a thread, a lock, data on the GPU, anything else that cannot be copied, and whatever does not fit in it: the snapshot's files together take up to 2 GB, and a single object of a type outside the list of personal copies (a polars table or a LightGBM booster, say) up to 64 MB. The console lists them once: “Not carried over: conn (database connection), batches (generator).” Hidden with its ×, the line comes back only when a new name or a new reason appears on the list, not with every snapshot. The same list takes the variables whose module the machine lacks: a package installed into the notebook's kernel alone (!pip install there), which no worker can load — “model (its module is only in the notebook's kernel)”. An attempt that reads such a variable is sent by the worker to the notebook's kernel — into the shared queue, at its place — and its card says why: “Run in the shared kernel: it uses conn, which cannot be carried over.” An attempt that reaches such a variable indirectly — through a function of the notebook, or globals()['conn'] — gets an understandable error, never someone else's data: “The variable conn was not carried over to the council machine (database connection), and this attempt reached it indirectly. Attempts whose text names conn run in the shared kernel.” It is not run again in the notebook's kernel. To have all of a cell's attempts run in the notebook's kernel, set its Attempts run to In the shared kernel, in turn. When the snapshot holds data on the GPU, the whole council runs in the notebook's kernel: workers get no GPU.

When attempts run the old way. An attempt runs in the notebook's kernel, in the shared queue, when the cell's Attempts run says In the shared kernel, in turn; while the pool is starting, its workers coming up and loading the snapshot; when no machine can take the pool, or the pool's machine does not answer or is lost; when the first snapshot did not come together or does not fit; when the snapshot holds data on the GPU; and when the attempt reads a variable that cannot be carried over. A refresh that did not come together or does not fit leaves the attempts on the previous snapshot, their cards say which cells it lacks, and the pool strip says why: “The new state could not be taken — attempts run on the state as of 14:05.” or “The new state does not fit — attempts run on the state as of 14:05.” The card of an attempt run in the notebook's kernel says why, for example “The pool is still starting — this attempt was run in the shared kernel.” If the pool did not start, the console says “The pool did not start: …” with the reason: at once when no worker started or loaded the snapshot — when no machine can take the pool, for example (“The pool did not start: The council pool runs only on machines.”), and then the cards say it too — and otherwise a minute after the first attempt that found the pool not ready, and then what follows the colon is what the pool still waits for: “the pool's machine does not answer”, “the notebook's state has not reached its machine yet”, “the workers have not loaded the notebook's state yet”, or the machine's own words about starting its container (“Starting the kernel on machine vast-3… image ↓ 45%”). The strip has no Refresh then: a new snapshot would not bring the pool up.

Failures.

  • The pool's machine is lost in the middle of an attempt: the attempt runs once more in the notebook's kernel, at its place in the queue, and the student is not charged the pause between runs: “The machine was lost in the middle of the attempt — it was run again in the shared kernel. The pause between runs was not counted.” The attempts that were waiting go there too. When the class notebooks run on that same machine — and the pool goes to them first, so with Everything on machines and Everything on a machine with a card they usually do — the notebook's kernel is lost too: the teacher's variables are gone, as in any move, these attempts run in the notebook's new kernel without them, and the pool starts from a snapshot of the new kernel. Run the setup cells again (with On Refresh only, press Refresh as well).
  • An attempt does not stop at its run limit: after the second interrupt the worker restarts itself and loads the snapshot again; the notebook's kernel and its queue are not touched. The pool restarts a worker the same way when an attempt left it threads, processes or more memory than Memory per attempt, or its Python died; for a minute the strip says worker restarted.
  • The notebook's kernel restarted: the workers go on with their snapshot; lost with its machine: the same, when the pool is on another machine.
  • The server restarted mid-attempt: each such attempt gets “The server restarted while the attempt was running. Its output is unavailable.”, and the workers restart before they run again.

How many workers — Attempts at once in Where it runs. With auto the pool has 2 to 16 workers: up to four at the opening, and while attempts wait the pool grows, up to one worker for every three people in the room and as many as fit in the machine's memory; with an empty queue it shrinks. A number of your own goes up to 32, a worker per student for example. Extra attempts wait in the pool's queue, with their place shown as before; your own runs go ahead of the students' attempts there. The pool stops 15 minutes after the notebook's last council cell closes.

In the console, such a council has a pool strip: how many workers are ready, how many are running and how many attempts wait (while the pool starts, “2 of 4 ready”), the pool's machine, the moment the snapshot is from and whether it follows the kernel, and Refresh; while a snapshot is being taken, it says taking a snapshot. The Queue tab shows the snapshot as the notebook queue's current run — how long it has been taken for and after which of your cells (“taking a snapshot · 0.4 s”, “after your cell 9”) — and the Rules sheet says how big the snapshot is and how long the last one took (“Now: state as of 14:05 · snapshot 1.2 GB, retaken in 0.4 s”). Times in the strip and on the cards are the browser's clock. When the notebook kernel's container was started before 0.25.0, without the class's PYTHONHASHSEED, the console says once: “The order of sets in attempts may differ from the notebook’s: its kernel was started before the update.” A run waiting for a worker is taken off the queue from the ⋯ menu of its work. Once an attempt has ended, its card — the author's and the console's — says under the output where and on which snapshot it ran, for example “computed apart · state as of 14:05” or “in the shared kernel: uses conn” (the whole sentence on hover), and lists its files: “attempt files: out.csv 12 KB”.

A format that works in class

Start with a short question, give time for individual answers, pick two different approaches and show them one after the other. Then bring the explanation back into the shared notebook and save the final correct version. Publishing the notebook does not export everyone's personal answers.