A notebook is a list of cells; the kernel is a separate Python process holding your variables. That split is the superpower — run one piece at a time and see the output inline — and the foot-gun, because cells run in whatever order you click.
The notebook is a document. The kernel is a process.
Cells are code or markdown; `.ipynb` is JSON holding the cells, their outputs and their execution counts. The kernel is a separate Python process that runs whatever cell you ask, in whatever order you click, keeping variables in memory until you restart it. The file can look right while the kernel disagrees — that gap is this lesson.
cells + outputs → the file · variables → the kernel02 / TWO MODES, ONE KEYBOARD
Shift+Enter is the heartbeat.
Escape puts you in command mode (blue bar): Shift+Enter runs a cell and moves on, A/B insert above/below, DD deletes, M/Y convert markdown/code, Z undoes, Ctrl+Shift+H lists everything. Enter puts you in edit mode (green bar): Tab autocompletes, Shift+Tab shows a signature, Ctrl+/ toggles comments. You will press Shift+Enter a thousand times a day.
Shift+Enter · A B DD · M Y · Tab / Shift+Tab / Ctrl+/03 / EXPLORE HERE, SHIP THERE
Notebooks explore. Scripts ship. Three traps in between.
Use notebooks for exploring data, prototyping models, visualizing results and explaining your work; use .py files for training pipelines, reusable utilities, anything a scheduler runs and production code. The three traps: out-of-order execution (fix: Kernel → Restart & Run All before sharing), hidden state (fix: restart the kernel), and memory leaks (fix: `del` + `gc.collect()`, or restart).
explore in notebooks → ship in scripts
MENTAL MODEL IN ONE SENTENCE
A notebook is a document executed against a separate memory — so the file can look perfectly right while the kernel disagrees; explore in the notebook, restart-and-run-all before you share it, and ship what works as a script.
By the end you will be able to explain what the kernel is and what a restart destroys; launch JupyterLab, Jupyter Notebook or VS Code against the same .ipynb file; run cells with Shift+Enter and navigate command/edit mode; read %timeit versus %%time correctly (many runs and a mean ± std versus one wall-clock reading); mount Drive in Colab and plan around the free tier’s 90 minutes of inactivity; decide notebook or script for a job; and diagnose the three traps — out-of-order execution, hidden state and memory leaks — with the first command for each.
01
CELLS AND THE KERNEL
A notebook is a list of cells. The kernel is a separate process.
Cells are the visible half: code or markdown, in file order, each with its own output underneath. The kernel is the half you cannot see: a Python process in the background that runs whatever cell you ask, in whatever order you click, and keeps your variables until you restart it.
Every artificial-intelligence (AI) paper, tutorial and Kaggle competition ships notebooks for one reason: you can run code in pieces and see the result inline. The source’s framing is blunt — trying to learn AI without a notebook is “doing math homework without scratch paper”. But a notebook is not a script with prettier output, and the difference matters enough to learn first.
Cell one: code. A code cell is Python, and the last expression in it is displayed automatically — no print required. The source’s example:
a code cell · run it with Shift+Enterpython
import numpy as np
data = np.random.randn(1000)
data.mean(), data.std()
Output: (0.0032, 0.9987) — a tuple, displayed because it is the last expression. Example output from the source.
Cell two: markdown. A markdown cell renders formatted text: headers, bold, italic, LaTeX math such as $E = mc^2$, tables and images. Use it to document what you are doing and why — that is the half of a notebook that makes it a lab notebook instead of a scratch pad. Markdown cells never touch the kernel, which is why they cannot cause a NameError; they only render.
Cell three+: rich output. The last expression can render as more than text. Put a pandas DataFrame as the last expression and Jupyter renders an HTML (hypertext markup language) table instead of a text dump — this is the four-row frame from the lesson’s notebook_tips.py:
rich output · a dataframe renders as an HTML tablepython
`print(df)` would give you monospaced text; typing `df` shows the table. Same data, different mime bundle.
What the reader sees, from the source’s example data — and the arithmetic a beginner should read out of it.
model
accuracy
train time
parameters
Linear Regression
0.72
0.1 s
102
Random Forest
0.89
2.3 s
50,000
Neural Network
0.94
45.6 s
1,200,000
XGBoost
0.91
8.2 s
25,000
Read those numbers like an engineer, because this is exactly what a notebook is for. The best accuracy is the neural network’s 0.94 — and it costs 45.6 s, which is 19.8× the random forest’s 2.3 s for +0.05 accuracy, with 24× the parameters (1,200,000 ÷ 50,000). XGBoost sits between: +0.02 over the forest for 3.6× the training time (8.2 ÷ 2.3). Whether any of that is a good trade is a modelling question; the point here is that the table, the timings and the parameters were on one screen, two keystrokes after the code that produced them. The source’s script prints the same conclusions in text — Best model: Neural Network, Fastest model: Linear Regression — but the dataframe is where your eye does the comparing.
Plots work the same way. In modern JupyterLab and Colab, %matplotlib inline is the default; the source teaches the line anyway, and it costs nothing to be explicit. Images render from memory or disk with the display helpers of IPython (Interactive Python), the kernel Jupyter grew out of.
The plot appears right below the cell; the image is read from disk and embedded. The source's claim: this is why notebooks dominate AI work — data, plot and code together.
And now the invisible half. The kernel is a Python process running in the background. When you run a cell, the interface sends the code to the kernel, the kernel executes it and sends back the result. Every cell in the notebook talks to the same kernel, so a variable defined in cell 2 is still in memory when you run cell 7 — and it is still there even if you never run cells 3 through 6. Three properties follow, and the source lists them: the kernel keeps variables in memory, it runs cells in whatever order you click, and it dies when you restart it.
That middle property is the superpower and the foot-gun at once. Click cell 6, then cell 2, then cell 7 and you have built a state the file does not describe. Restart the kernel and all of it is gone: the variables, the imports, the model you just trained — while the notebook file still shows every output as if nothing happened.
The round trip: UI → kernel → output
The notebook file is a document; the kernel is a separate program holding your variables. Watch four cells make the trip — then flip the toggle and let the kernel restart before every cell.
the protocol, one cell at a time
1 UI serialises the cell and sends execute_request
2 the kernel runs the code in its own process
3 it replies with execute_reply: output, or an error
4 the UI renders the reply under the cell
ONE KERNEL — what accumulates:
cell 1 → kernel now holds np
cell 2 → kernel now holds np, data
cell 3 → uses data, adds nothing (persists!)
cell 4 → uses data again — it is still there
Restarting the kernel is the only thing that wipes it.
“Variables persist between cells” is a statement about the kernel, not the file. The same notebook, opened with a fresh kernel, starts with none of them.
Quick check
You run three cells in a row, then press Kernel → Restart. What is still true?
02
THREE WAYS IN, ONE FILE
Three interfaces. One .ipynb format.
JupyterLab, Jupyter Notebook and VS Code look like three products and are one file format. Pick the one whose editor you already like; nothing you write is locked in, and the format itself is plain JavaScript Object Notation (JSON).
The name Jupyter honors the project’s first three languages — Julia, Python and R — even though an AI workflow is almost always Python. The three interfaces below are the ones the source lists, with its install lines and its summary of who each one is for:
Interface
Install
Best for
JupyterLab
pip install jupyterlab then jupyter lab
Full IDE experience, multiple tabs, file browser, terminal
Jupyter Notebook
pip install notebook then jupyter notebook
Simple, lightweight, one notebook at a time
VS Code
Install the “Jupyter” extension
Already in your editor, git integration, debugging
All three read and write the same .ipynb file, so “pick one” is not a commitment — and moving a notebook between them is an open, not an export. JupyterLab — the full integrated development environment (IDE) option — is the most common in AI work, which is why the rest of this lesson shows its menus and shortcuts; VS Code users get the same cells and the same kernel behind a different chrome.
launch JupyterLab in the course environmentbash
pip install jupyterlab
jupyter lab
# the lightweight alternative
pip install notebook
jupyter notebook
Verbatim from the source. Install into the project's virtual environment (Phase 0, Lesson 06), not the system Python — the kernel that runs your cells is the interpreter that has jupyterlab installed.
One habit pays off immediately: put pip install work in the environment the notebook runs in. A notebook is only ever as good as its kernel’s packages — if import torch fails in a cell, the question is not what is installed on your machine but what is importable by that kernel.
Inside a .ipynb file
A notebook file is one JSON document: cells, their outputs, their execution counts, and metadata. Open it like a text file and this is what you are looking at — JupyterLab, Jupyter Notebook and VS Code are three different renderers for the same JSON.
One JSON document. `cells` is the notebook; `metadata` names the kernel that was used, not the kernel itself; `nbformat` is the file-format version.
inside the file
cells[] the notebook, in order
cell_type "code" or "markdown"
source[] the text of the cell
outputs[] mime bundles: text/plain, text/html,
image/png (base64, +33% size)
execution_count the In [4] badge — saved, not live
metadata kernelspec, language info, versions
not in the file
variables np, data, losses — they live in the
kernel's memory, not in the JSON
the kernel a process, started when the notebook
opens; the file only names its type
run order only execution_count hints at it
This is the mechanical reason the three traps exist: the file stores what you saw (outputs, counts) while the kernel holds what you have (variables). The two can disagree — the file does not check.
Everything you just clicked through has a consequence worth stating plainly: the notebook file stores your evidence, not your state. Outputs, execution counts and cell sources are in the JSON; the variables that produced them live in a process the file only names. That is why a notebook can be reopened hours later with the same plots on screen and none of the objects in memory, and why “the outputs look right” can never substitute for “the file replays from a fresh kernel”.
03
TWO MODES, ONE KEYBOARD
One mode where letters are commands. One where letters are letters.
A notebook cell is always in one of two modes. Escape puts you in command mode — the blue bar on the left — where single keys operate on the cell. Enter puts you in edit mode — the green bar — where the keyboard types Python. The same key means different things; the bar tells you which world you are in.
This is the one part of the notebook you should learn by muscle memory rather than by menu. The source gives two short tables, and Shift+Enter is the one you will use a thousand times a day — learn it first.
Command mode · the blue bar · from the source’s table.
Key
Action
Shift+Enter
Run the cell, move to the next
A
Insert a cell above
B
Insert a cell below
DD
Delete the selected cell
M
Convert the cell to markdown
Y
Convert the cell to code
Z
Undo the last cell operation
Ctrl+Shift+H
Show every shortcut
Edit mode · the green bar · three keys carry the day.
Key
Action
Tab
Autocomplete the name you are typing
Shift+Tab
Show the function’s signature
Ctrl+/
Toggle comments on the selected lines
Why two modes at all? Because the alternative is a menu for everything:DD deletes a cell in two keystrokes, and A / B let you grow a notebook as fast as you can think. The price is that D is not “delete” until you press it twice — the first press only arms it — so a stray keystroke in command mode is recoverable with Z or Escape. Nothing about a notebook’s state changes when you switch modes; modes are about the keyboard, not the kernel.
Shortcut trainer
A mock notebook. Click into the board (or Tab to it) to give it keyboard focus, then press the real keys — Enter, Escape, Shift+Enter, A, B, DD, M, Y, Z, Ctrl+Shift+H. Every action has a button too, because shortcuts should never be the only way.
MOCK NOTEBOOK · 4 CELLS · SELECTED 1COMMAND MODE · BLUE BAR
mode command
selected cell 1 of 4 · markdown
runs none yet
undo stack 0 snapshots
last action
Focused in command mode. Press Shift+Enter to run the selected cell.
Two modes, two alphabets. In command mode a bare letter is a command; in edit mode the same letter is just a letter. That is why the mode bar matters more than any single shortcut.
Quick check
You are in command mode, and you want to turn the selected code cell into a markdown cell. Which key?
04
MAGIC COMMANDS
Not Python. Not typing either. Environment control.
Magic commands are Jupyter-specific instructions, and the syntax tells you their reach: %name is a line magic that acts on one line, %%name is a cell magic that acts on the whole cell. Inside a notebook they are how you time code, draw plots, install packages and read the environment.
The two timing magics are the pair everyone meets first, and they answer different questions. The source’s examples, with their outputs:
microbenchmark · %timeit runs it many times and averagespython
%timeit np.random.randn(10000)
Output: 45.2 us +/- 1.3 us per loop — the source's example reading. `us` is microseconds (µs).
one training run · %%time runs it oncepython
%%time
model.fit(X_train, y_train, epochs=10)
Output: Wall time: 2.34 s — the source's example reading. One run, one number.
The rule is one sentence: %timeit runs the code many times and averages; %%time runs it once. Use the first for microbenchmarks and the second for training runs. The reason is worth the arithmetic, because the source’s single line hides a real scheduling decision. %timeit first grows an internal loop count until one timing run lasts at least about 0.2 seconds, then repeats the measurement seven times (its default) and reports the mean and standard deviation.
%timeit np.random.randn(10000)
per call 45.2 us +/- 1.3 us (source's example reading)
loops each 10,000 (grow the count until one run ≥ 0.2 s)
one timing run 10,000 × 45.2 us = 0.452 s
all runs 7 × 0.452 s = 3.16 s (7 = timeit's default repeat)
± mean 1.3 / 45.2 = 2.9% (the ± is one standard deviation)
bytes per call 10,000 float64 × 8 bytes = 80 KB
churn all runs 7 × 10,000 × 80 KB = 5.6 GB (freed per loop — churn, not a leak)
%%time model.fit(X_train, y_train, epochs=10)
one run 2.34 s (Wall time) (source's example reading)
as %timeit 7 × 2.34 s ≈ 16.4 s — and the model trains 7 times
scale check 2.34 s / 45.2 us ≈ 51,770 — a training run is ~50,000× the microbenchmark
Two things follow. First, a number without its spread is a rumour: the ±1.3 us is what separates “45 µs” from “somewhere between 40 and 50 µs”, and the mean of seven runs is more trustworthy than any single reading. Second, the cost of that trust is time — 3.16 seconds for a microbenchmark is nothing, but the same magic on a 2.34-second training run re-trains the model seven times and burns roughly 16 seconds of compute for a wall-clock number you could have had once. The numbers above are the source’s example readings; the loop arithmetic beside them is derived from timeit’s documented behaviour, so run it on your own machine and compare.
The other magics in this lesson each replace a trip out of the notebook:
plots, packages and the environmentpython
%matplotlib inline
# every plt.plot() / plt.show() now renders directly in the notebook
!pip install scikit-learn
# the ! prefix runs any shell command
%env CUDA_VISIBLE_DEVICES
# read an environment variable — here, which GPU is visible
All three are verbatim from the source. CUDA is Compute Unified Device Architecture, NVIDIA's GPU platform, and CUDA_VISIBLE_DEVICES is the variable that decides which cards a process can see. The ! escape is the bridge to the shell: !ls, !pwd, !nvidia-smi are the same mechanism.
One caveat about !pip install: it installs into the kernel that is currently running, which is convenient in Colab and a common source of “it worked in the notebook but not in my script”. In a local project, prefer installing into the environment from a terminal (Phase 0, Lesson 06), and remember that a kernel keeps the modules it already imported — a package installed from inside a cell can still need a kernel restart before the new version is visible.
The magic-command playground
Pick a subject and a timing magic. The numbers are the source’s example readings (45.2 ± 1.3 µs for the array, 2.34 s for the fit); the loop arithmetic beside them is computed live — a simulation, not a benchmark of your machine.
THE CELL · LINE MAGIC %
%timeit np.random.randn(10000)timeit
%timeit np.random.randn(10000)
WHAT JUPYTER PRINTS
45.2 us +/- 1.3 us per loop
(mean +/- std. dev. of 7 runs, 10,000 loops each)
one timing run 10,000 loops × 45.2 us = 452.0 ms
all timing runs 7 × 452.0 ms = 3.16 s of wall clock
one loop 80 KB allocated (10,000 float64)
churn per run 10,000 × 80 KB = 800 MB
churn, all runs 7 × 800 MB = 5.6 GB (freed each loop — churn, not a leak)
Verdict · This is what %timeit is for: a sub-millisecond operation measured many times so the noise averages out. The price is a few seconds of wall clock for one trustworthy number.
the source's microbenchmark subject: one 10,000-element float64 array per call = 80 KB.
runs the statement many times and reports mean ± std. A line magic acts on one line; a cell magic (%%time, %%timeit) acts on the whole cell.
subject np.random.randn(10000)
single call 45.2 us (source's example reading)
magic %timeit · many runs, mean ± std
loops each 10,000 (timeit grows the loop count until one run lasts ≥ 0.2 s)
wall clock 3.16 s for the whole measurement
runs 7
mean reading 45.2 us +/- 1.3 us
noise share 2.9% of the mean (± is one standard deviation)
rule of thumb %timeit for microbenchmarks · %%time for training runs
The loop-count scan is the part beginners miss: %timeit does not run your statement once — it repeats it until a run is long enough to time, then repeats the whole measurement 7 times. That is a gift for a 45 µs call and a hazard for a training loop.
Quick check
You are about to train a model for about two minutes and want an honest wall-clock number. Which magic, and why?
05
NOTEBOOKS IN THE CLOUD
No GPU? Borrow one. The runtime is temporary.
Colab is a free Jupyter notebook in the cloud: a graphics processing unit (GPU) — a T4 on the free tier — plus pre-installed libraries and Google Drive integration, with no setup. The trade is that the machine is borrowed and temporary, so you plan around two numbers: 90 minutes of inactivity, and no disk that survives.
The launch path from the source is three steps long: go to colab.research.google.com, upload any .ipynb file from this course, then choose Runtime → Change runtime type → T4 GPU (free). Because a Colab notebook is the same .ipynb format, this is an open, not a conversion — the notebook you wrote locally gets a different machine underneath it.
Open colab.research.google.com and sign in with a Google account.
Upload an existing .ipynb (File → Upload notebook) or start a new one.
Runtime → Change runtime type → T4 GPU, then prove the machine from a cell with !nvidia-smi.
A T4 is a real accelerator, not a demo. The free-tier card carries 16 GB of video memory — enough for the small model experiments in the early phases and for reading other people’s notebooks — and its harness lists the card, its driver and the memory it found. That last part is worth internalising: when a cloud notebook underperforms, the first diagnostic is not the code, it is !nvidia-smi saying which device the kernel actually has. (The 16 GB is the card’s datasheet specification; what Colab offers on any given day is Google’s call.)
The differences from local Jupyter are all consequences of one fact — the runtime is temporary:
Difference
What it means for your work
Files do not persist between sessions
Anything written to /content disappears. Save to Drive or download.
Pre-installed libraries
numpy, pandas, matplotlib, torch, tensorflow and sklearn are ready on a fresh runtime — no install cell needed.
from google.colab import files
Upload and download files from a cell.
drive.mount('/content/drive')
Persistent storage: your Drive becomes a folder the runtime can read and write.
Sessions time out after 90 minutes of inactivity (free tier)
An idle tab is a countdown. Checkpoint to Drive as you go, and expect the next visit to begin with a fresh runtime.
Ninety minutes is generous for reading a notebook and tight for training a model, and the difference is not about discipline: a closed laptop lid does not pause a Colab runtime the way it pauses a local kernel. The two-line habit that turns Colab from a scratch pad into a workspace:
make Colab survive its own sessionpython
from google.colab import drive
drive.mount('/content/drive')
# then write outputs somewhere that outlives the runtime:# /content/drive/MyDrive/experiments/run-04/model.ptfrom google.colab import files
files.download('model.pt') # or take a copy down with you
Both imports are from the source's Colab section. Mount early — a mount prompt can appear minutes after a cell asks for Drive, and an unattended session can time out waiting for it.
06
EXPLORE HERE, SHIP THERE
A notebook to find out. A script to keep.
Notebooks are brilliant at exploration and bad at being programs: they have no entry point, they run in whatever order you click, and their state lives in a process. The rule that resolves every “where does this go?” question is one line — explore in notebooks, ship in scripts.
The source’s table is worth reading twice, because the two columns are not “easy” and “hard” — they are interactive and unattended:
Use notebooks for
Use scripts for
Exploring a dataset
Training pipelines
Prototyping a model
Reusable utilities
Visualizing results
Anything with if __name__
Explaining your work
Code that runs on a schedule
Quick experiments
Production code
Course exercises
Packages and libraries
The left column has a human in the loop, re-running one piece, reading output, changing a line; the right column has a machine — a scheduler, an importer, a server — that needs a program with a beginning, an end and no clicking. The four-step workflow AI teams actually follow makes the handoff concrete:
Explore the data in a notebook.
Prototype the model in the notebook.
Once it works, move the code into .py files.
Import those .py files back into the notebook for the next round of experiments.
Step 4 is the one beginners skip, and it is what makes the workflow a loop instead of a one-way exit: the notebook keeps being the workbench, but the parts that proved themselves stop being cells and become importable modules. Concretely, the feature engineering from yesterday’s notebook becomes features.py, and the notebook now reads:
the handoff · a notebook that imports its own production codepython
# features.py — the cell moved into a file, with a real namedef build_features(df):
...
# experiment.ipynb — step 4: use the module from the notebookfrom features import build_features
X = build_features(df)
The source's workflow in one snippet. Note the gotcha this introduces: Python caches imports, so a running kernel will not pick up an edit to features.py — restart the kernel, or use IPython's %load_ext autoreload / %autoreload 2 pair (a practical detail beyond the source's four steps).
One more judgement call the table implies: a snippet becomes a script when a machine has to run it, not when it crosses some line count. Ten lines a scheduler runs every night is a script; two hundred lines you are still poking at are a notebook. And the reverse migration is normal too — a debugging session on a broken pipeline often moves the pipeline’s functions into a notebook temporarily, where you can watch intermediate values. The code should be the same; only the container changes.
Notebook or script?
The source’s table, turned into a drill: twelve jobs, two homes. Sort each one — the answer explains itself as soon as you commit.
Explore a dataset you have never seen
Prototype a model and watch the curves
Visualize this week's evaluation results
A one-off %timeit comparison of two functions
A course exercise in this curriculum
The training pipeline that must run every night
A data-loading function three other files import
Anything with if __name__ == "__main__"
A job cron runs at 06:00
The endpoint that serves predictions in production
A package you publish for other people to install
Load the checkpoint and print its accuracy, one time
sorted 0 / 12
correct 0
rule Notebooks explore; scripts ship.
the test that decides most cases
Will a human re-run one piece at a time and read the
output? → notebook
Will a machine (or another file) import or schedule it?
→ script
the source's workflow
1 explore the data in a notebook
2 prototype the model in a notebook
3 the moment it works, move the code into .py files
4 import those .py files back into the notebook
for the next round of experiments
“Explore in notebooks, ship in scripts” is not a preference, it is a handoff: the notebook is where the idea becomes reproducible, and the script is what reproducibility gets handed to.
07
THE THREE TRAPS
The notebook ran fine. That is the problem.
Every notebook trap has the same shape: the evidence on screen is real, but the state that produced it cannot be reconstructed. There are three of them, and each has a one-command first fix.
Trap one: out-of-order execution. You run cell 5, then cell 2, then cell 7. The output is correct and the file looks fine — but the notebook now depends on an order that exists only in your fingers. Someone opens the file, runs it top to bottom, and the replay stops with a NameError: a cell above uses something a cell below creates, and the gap was invisible while your kernel kept the value alive. The source’s fix is one command, run before you share anything: Kernel → Restart & Run All. If the replay passes, the file is the notebook; if it fails, you have found a dependency your clicks were hiding.
Trap two: hidden state. You delete a cell — the one that defined scaler, say — and the notebook keeps working, because the variable it created is still in the kernel’s memory. The file now looks clean and depends on a ghost: no cell creates scaler, yet every cell that uses it runs. A fresh kernel is where the ghost shows itself, with the same NameError the source describes. The distinction from trap one is worth holding onto: out-of-order state is in the file, in the wrong place; hidden state is not in the file at all. The fix for hidden state is to restart the kernel regularly — it is the only way to see what the file really provides — and then restore the missing definition into a cell.
Trap three: memory leaks. Load a 4 GB dataset, train a model, engineer features — say the training step added 2 GB — then load a second 4 GB dataset: nothing gets freed, because every object is still referenced by a kernel variable. Python’s memory manager cannot reclaim what your variables still point at, and the kernel is a long-lived process by design, so the numbers add up instead of resetting: 4 GB + 2 GB + 4 GB = 10 GB resident, on a laptop that has to page. Cells that ran in two seconds start taking forty. The source’s first fix is explicit — del the variables you are done with, then gc.collect():
free what you are done with, then sweeppython
del df, features # drop your referencesimport gc
gc.collect() # sweep reference cycles# the alternative, always available: Kernel → Restart# and, for a multi-dataset pass, load one dataset at a time
From the source's memory-leak fix. Restarting the kernel frees everything at once; `del` + `gc.collect()` is the surgical version when you want to keep the session.
To feel the scale of that one, the source’s own notebook_tips.py prints its arrays’ sizes: 1,000 elements is 0.008 MB, 100,000 is 0.8 MB, and np.random.randn(10_000_000) is 80 MB (10,000,000 × 8 bytes for float64). That is one array in a fresh kernel. Now imagine holding three 4 GB dataframes because the second analysis might need the first one’s raw form — a habit that costs nothing in a script that exits, and gigabytes in a kernel that stays open all afternoon.
Notebook execution simulator
Seven cells, one kernel. Run them in any order and watch the kernel panel fill or miss variables. This is a simulation of a notebook — the arithmetic is real, the cells are fixed.
kernel memory empty
variables 0 / 4
order watch top-to-bottom so far
executions 0
restarts 0
last events
— no cells run yet
A NameError is a kernel fact, not a notebook fact: the code is fine, the variable simply never entered memory in this session. Markdown cells render without touching the kernel — that is why they can never cause one. One distinction from the source’s story: the simulation starts from an empty kernel, so the 5 → 2 → 7 clicks fail immediately — in the source’s version those clicks worked on the machine that built that state, and the break arrived when someone replayed the file top to bottom.
The trap detector
Three state stories, three fixes. For each one, name the trap and choose the first thing you would do. “It works on my machine” is not a diagnosis — the state is the diagnosis.
what you seestory 1 / 3
cell 9 plot_losses(losses) # 0.94 AUC (area under the ROC curve), the plot looks right
cell 11 losses = history[...] # definitions moved down while tidying
teammate: Kernel → Restart & Run All
cell 9 → NameError: name 'losses' is not defined
1 · WHICH TRAP IS THIS?
2 · WHAT IS THE FIRST FIX?
stories solved 0 / 3
trap — not chosen
first fix — not chosen
Pick both, then check. The three traps overlap in their
symptoms ("it worked before"), so the diagnosis has to
name where the state lives: the click order, the kernel's
memory, or the machine's RAM.
Out-of-order and hidden state look identical from the outside; what separates them is whether the defining cell still exists in the file.
Quick check
A teammate's Restart & Run All fails in cell 9 with `NameError: name 'losses' is not defined`, while your screen still shows a correct plot from cell 9. What is the first thing to check?
08
CHECK YOURSELF
Six questions. Then the terms worth keeping.
Answer before you look. The kernel question and the works-on-my-machine question are the two that decide whether you can debug a notebook or only re-run it, and the exercises end with a triage drill that puts all three traps in one hand.
0 / 6 answered · 0 correct
01What is a Jupyter notebook?
02What does the Jupyter kernel do?
03What is the difference between %timeit and %%time magic commands?
04What is the most common cause of a notebook working on your machine but failing when someone runs it top to bottom?
05When should you move code from a notebook into a .py script?
06Which of these statements about a free Google Colab session is true?
Key terms, demystified
Click a card to swap the lazy description for what it actually means.
Exercises from the lesson
Four problems with exact commands — benchmark list versus NumPy, build a notebook that survives Restart & Run All, run the bundled tips file on a free Colab GPU, and triage three notebooks whose state has gone wrong. Try first; a worked answer is one click away.
Open JupyterLab, create a notebook, and use %timeit to compare a list comprehension with NumPy for making an array of 100,000 random numbers (the source's Exercise 1).Show one worked answer
Launch it (`pip install jupyterlab`, then `jupyter lab`), then in the first cell run two microbenchmarks with separate cells so the readings are paired with the code: `import random`, then `%timeit [random.random() for _ in range(100_000)]` and, after `import numpy as np`, `%timeit np.random.randn(100_000)`. What the numbers should look like — and this is illustrative, your machine decides: the list version calls Python's `random.random` 100,000 times, once per element, so it lands in the milliseconds; NumPy builds the whole array in one C call, so it lands in the hundreds of microseconds; the gap is typically tens of times in this microbenchmark (your machine decides the exact factor), which is the same story the source's own `notebook_tips.py` prints for squaring 1,000,000 elements (`List comprehension: …s`, `NumPy: …s`, `Speedup: …x`). Three details worth noticing while you run it: (1) `%timeit` re-runs each statement thousands of times, so the two readings are means, not single samples — the `±` is the standard deviation across the timing runs; (2) the list version creates 100,000 Python float objects, while NumPy allocates one 800 KB buffer (100,000 × 8 bytes), which is the real difference the benchmark is measuring; (3) when you want the same comparison for a block of code instead of one line, use %%time or %%timeit as a cell magic — and for a training run, %%time, because %timeit would repeat it.
Create a notebook with both markdown and code cells that loads a CSV, displays a dataframe, and plots a chart. Then run Kernel → Restart & Run All to verify it works top to bottom (the source's Exercise 2).Show one worked answer
A five-cell notebook that does exactly that: (1) markdown — `# First look` plus one sentence on what the file is; (2) code — `%matplotlib inline` and `import pandas as pd`; (3) code — `df = pd.read_csv("data.csv")` then `df.head()` as the last expression, so Jupyter renders the HTML table instead of a text dump (`print(df.head())` would give the text); (4) markdown — `## The distribution`, the question this chart answers; (5) code — `df["value"].plot.hist(bins=30)` (or `plt.hist(df["value"], bins=30)` with `import matplotlib.pyplot as plt`). Do not trust the outputs you already see: press Kernel → Restart & Run All and let it replay from an empty kernel. If it passes, the notebook is shareable; if cell 5 fails with `NameError: name 'plt' is not defined`, the import was typed in a cell that never ran during a fresh replay — move it into cell 2 where it belongs. One nuance: `%matplotlib inline` is the default in modern JupyterLab and Colab, so the plot may appear without it; including it costs nothing and makes the notebook explicit about how the figure should render.
Take the code from code/notebook_tips.py, paste it into a Colab notebook, and run it with a free GPU (the source's Exercise 3).Show one worked answer
Open colab.research.google.com, choose File → New notebook, and paste the functions (or upload the `.ipynb`). Then Runtime → Change runtime type → T4 GPU, and prove the machine with `!nvidia-smi` — the `!` prefix runs any shell command from a cell, which is the same escape hatch as `!pip install`. The four demos run as written, with one edit worth making: in `inline_plotting` the script calls `matplotlib.use("Agg")` before importing pyplot, because it is designed to run headless and save `notebook_plot.png`; under Agg, `plt.show()` is a no-op that warns, so in Colab delete that line (or keep it and download the PNG instead). The other demos — the list-versus-NumPy timing, the dataframe display, and the memory check that prints 0.008 MB / 0.8 MB / 80 MB for the 1K/100K/10M arrays — run unmodified. Two Colab-specific facts to plan around: nothing on the runtime's disk survives the session, so save anything you care about with `from google.colab import drive; drive.mount('/content/drive')` and write there, or download with `files.download(...)`; and the free tier disconnects after 90 minutes of inactivity. For a 30-minute lesson that is generous — for an 8-hour training run it is a trap, and that is the point of the exercise.
Three readings arrive from three different notebooks: (a) all outputs are correct, but Restart & Run All stops at cell 9 with `NameError: name 'losses' is not defined`, and cell 11 defines `losses`; (b) cells run fine and a teammate's fresh kernel fails at cell 7 with `NameError: name 'scaler' is not defined`, and no cell in the file defines `scaler`; (c) the kernel's memory sits near 10 GB after three loads of 4 GB, 2 GB and 4 GB, and cells got 20× slower. Name each trap and the first command you would run. (Original to this page — the source states the traps; this exercise makes you triage them.)Show one worked answer
(a) Out-of-order execution. The dependency exists in the file, but below its use: cell 11 creates what cell 9 needs. Your machine's kernel already held `losses` because you clicked cell 11 earlier; the replay reaches cell 9 first. First command: reorder the cells so definitions sit above their uses, then Kernel → Restart & Run All — the replay is the test, and it should now reach the end. (b) Hidden state. The defining cell was deleted after it ran, so the dependency exists only in the kernel's memory. The file cannot be fixed by reordering because there is nothing to reorder. First command: Kernel → Restart, which removes the ghost and makes the failure immediate and reproducible; then restore the definition (rewrite the cell, or recover it from git history) and re-run top to bottom. (c) Memory leak. Every object is still referenced by a kernel variable: 4 + 2 + 4 = 10 GB resident, and `features` keeps `df` alive even if you only intended to keep the features. First command: `del df, features, df2` followed by `gc.collect()` — or, cleaner for a next pass, Kernel → Restart and load the datasets one at a time. The common thread: in every case the fix starts by making the state visible — restart, replay, or free — before touching the code. And a practical detail: `%whos` (or `%who`) lists the kernel's variables when you have lost track of what is alive.
Terms this lesson borrows from later lessons (or outside)
You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.
virtual environment — The per-project box of packages a notebook's kernel should be installed into, so `pip install jupyterlab` and `!pip install scikit-learn` land in the course environment rather than a system interpreter. Set up properly in Phase 0, Lesson 06 (Python Environments).
GPU / T4 / CUDA — The free Colab tier hands you a T4 — and what a GPU is, how CUDA and Apple's MPS (Metal Performance Shaders) differ, and when a run belongs on one is Phase 0, Lesson 03 (GPU Setup & Cloud). This lesson only borrows the machine for a notebook.
pandas DataFrame — The table type behind the lesson's HTML output: `pd.DataFrame({...})` printed as a formatted table, not a text dump. Loading, cleaning and joining — the work this lesson calls “exploring a dataset” — gets its treatment in Phase 0, Lesson 09 (Data Management).
garbage collection (`gc.collect()`) — Python frees memory when the last reference to an object goes away; `del` removes one reference and `gc.collect()` forces a sweep for reference cycles. Debugging exactly this — what is holding memory and why — is Phase 0, Lesson 12 (Debugging and Profiling).
training epochs (`model.fit`) — The 2.34 s operation being timed in the magic-command examples: one fit over ten epochs. What `fit` does internally — gradients, losses, optimizer steps — starts in Phase 2 (ML Fundamentals) and becomes explicit in Phase 3, Lesson 11 (Introduction to PyTorch).
KEEP GOING
A picture is a start. Practice is the rest.
This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.
Lesson text adapted from AI Engineering from Scratch (Phase 00, Lesson 05) and its `code/notebook_tips.py`. Everything the source states is kept as-is: the notebook as a list of code and markdown cells, the kernel as a separate Python process that keeps variables and dies on restart, the three interfaces with their install and launch commands, the two modes with the shortcut tables, the magic commands with their `45.2 us +/- 1.3 us per loop` and `Wall time: 2.34 s` example outputs, the pandas / matplotlib / IPython.display rich-output snippets, the Colab differences (free T4, pre-installed libraries, Drive mount, no persistence, 90 minutes of inactivity), the notebooks-versus-scripts table with the “explore in notebooks, ship in scripts” rule and the four-step workflow, and the three traps with their fixes. Original to this page: the five labs (the execution simulator, the kernel round-trip animator, the shortcut trainer, the magic-command playground and the trap detector) plus two extra boards (the .ipynb anatomy explorer and the notebook-or-script sorter); the second fully worked example, the four-row model comparison (0.94 accuracy at 45.6 s versus 0.89 at 2.3 s: +0.05 accuracy for 19.8× the training time and 24× the parameters); the %timeit arithmetic behind the source's one line (10,000 loops each, 0.452 s per timing run, ≈3.16 s for seven runs, 80 KB per call and ≈5.6 GB churned); the 4 + 2 + 4 = 10 GB memory-loss arithmetic next to the source's own 0.008 / 0.8 / 80 MB array printout; the “the file is the story, the kernel is the state” memory hook; the Colab paper-reproduction scenario; the import-cache / %autoreload gotcha in the four-step workflow; and the sixth quiz question (Colab sessions). Every timing figure is the source's, or a derivation from timeit's documented loop-count rule — both are teaching numbers, not a benchmark of your machine.