Notebooks · AI-native by design

Jupyter Notebooks and Markdown

A Jupyter notebook stores its documentation as raw Markdown strings inside a plain JSON file. That design decision, made in 2011, turns out to align exactly with how language models read and write text.

Jupyter notebooks have two surfaces. The rendered view in a browser shows formatted headings, executed code, plots, and output. Underneath that view is a plain JSON file — open it in a text editor and the structure is immediately legible. The documentation sections are stored as raw Markdown, exactly as written, not preprocessed or converted into anything else. That plainness is not a limitation. It is the reason Jupyter notebooks are one of the most AI-native document formats in wide use.

Illustration: layered blocks representing notebook cells, human figure assembling them

Most document formats hide their content behind a layer of encoding. A Word document is a ZIP archive full of XML. A PDF is a stack of instructions for a page description language. A Jupyter notebook, by contrast, is just JSON. Open it, read it, edit it with any tool that handles text. The documentation cells are Markdown strings. The code cells are source code strings. The outputs are whatever the kernel produced. Everything is there, and nothing requires special software to inspect.

This openness is why language models interact with notebooks more naturally than with almost any other document format. But to understand why, it helps to start with where the format came from.

From IPython to Jupyter

Fernando Pérez created IPython in 2001 as a better interactive shell for Python — one that kept a history of commands and made it easy to experiment without writing full scripts. For nearly a decade it was a command-line tool. Then, in 2011, a browser-based interface called the IPython Notebook was added. You could now write code in a browser, execute it, and see the output directly below the cell. Prose and code could sit on the same page.

In 2014 the project was renamed Jupyter — a portmanteau of Julia, Python, and R, the three languages it first supported. Today it supports dozens of languages through a kernel system, but the core idea has not changed: an interactive document where prose and executable code take turns.

"The IPython Notebook is a web-based interactive computational environment where you can combine code execution, text, mathematics, plots and rich media into a single document."

— IPython Notebook announcement, 2011

The file format for storing notebooks was called .ipynb from the start — short for IPython Notebook. The name stuck through the rename. A .ipynb file is a Jupyter notebook regardless of which language it contains or which version of Jupyter created it.

Two cell types, one document

A notebook is a sequence of cells. Each cell has a type. For most purposes, only two types matter:

Cell types

  • Code cells Contain executable source code. When you run a code cell, the kernel executes it and any output — text, numbers, tables, plots, error messages — is stored directly below the cell in the notebook. Code cells carry a counter showing the order in which they were executed.
  • Markdown cells Contain documentation written in Markdown. When rendered, they appear as formatted prose: headings, bullet points, bold text, links, inline code, and LaTeX math. When raw, they are just Markdown source. Markdown cells do not execute. They exist purely to explain what the code cells do, provide context, or structure the notebook into readable sections.
  • Raw cells Pass-through content for tools like nbconvert. Not commonly used in everyday notebooks.

The interleaving of Markdown and code is the central idea. A well-written notebook reads like a document: an introduction, a section of code that does something, a Markdown explanation of the result, more code, more explanation. The notebook becomes both the working environment and the final report. This style — sometimes called literate programming — was articulated by Donald Knuth in the 1980s as an ideal that most programming environments never quite achieved. Jupyter, accidentally or not, came closer than most.

Inside the format: .ipynb is JSON

Open any .ipynb file in a text editor and the structure is immediately visible. The file has a top-level "cells" array and a "metadata" object. Each element of the cells array is a JSON object with a "cell_type" field, a "source" field, and a handful of others depending on type.

Here is what a Markdown cell looks like on disk:

Markdown cell — raw JSON
{
  "cell_type": "markdown",
  "metadata": {},
  "source": [
    "## Data cleaning\n",
    "\n",
    "Before fitting the model, we remove outliers using a 3-sigma cutoff.\n",
    "This step is critical — see the referenced paper for full justification.\n",
    "\n",
    "- Values more than 3 standard deviations from the mean are dropped.\n",
    "- The threshold was chosen empirically on the validation set."
  ]
}

The "source" field is an array of strings, one per line of Markdown, each ending in \n. That is the complete representation of the documentation cell. No encoding, no transformation, no binary data. It is Markdown, stored as Markdown, inside JSON.

A code cell that follows it looks structurally similar, with the addition of output storage:

Code cell — raw JSON
{
  "cell_type": "code",
  "execution_count": 4,
  "metadata": {},
  "outputs": [
    {
      "name": "stdout",
      "output_type": "stream",
      "text": ["Removed 23 outliers. 4977 rows remaining.\n"]
    }
  ],
  "source": [
    "threshold = df['value'].mean() + 3 * df['value'].std()\n",
    "removed = df[df['value'] > threshold]\n",
    "df = df[df['value'] <= threshold]\n",
    "print(f'Removed {len(removed)} outliers. {len(df)} rows remaining.')"
  ]
}

The outputs of executed cells are also stored in the file. This means a notebook saved after execution contains not just the code and documentation, but also a snapshot of every result. A reader who opens the file without a running kernel can still see what the code produced when it was last run.

Why language models speak notebook natively

Language models are trained on large amounts of text scraped from the public internet. GitHub is a substantial part of that training data, and GitHub hosts millions of .ipynb files — data science tutorials, research code, machine learning experiments, university course materials. The notebook format appears in training data at scale, in its raw JSON form.

This means language models have seen the structure repeatedly. They know that "cell_type": "markdown" is followed by prose, that "cell_type": "code" is followed by executable source, and that the two alternate in a logical progression. More importantly, the documentation cells are written in Markdown — the same format that dominates the rest of the training data. There is no translation step. The model reads a notebook the same way it reads a README or a blog post: as structured text.

Markdown is particularly well-suited to this because it is plain text formatted — the semantics of the document are visible in the source itself. A heading is a heading because it starts with #, not because a stylesheet says so. A code sample is marked as code because it is in backticks, not because of invisible binary formatting. This makes Markdown predictable for a model that has to generate it — there is no gap between what the model writes and what the format requires.

"Markdown is the format that works equally well as input to a language model and as output from one. Jupyter notebooks store their documentation in exactly that format."

The combination matters: Jupyter notebooks interleave the format language models understand best (Markdown) with executable code that is also well-represented in training data. The result is that a language model can reason about a notebook — what the code does, whether the documentation accurately describes it, what the next logical step might be — without any special preprocessing.

Generating notebooks with AI

Because the format is plain JSON, you can ask a language model to generate a complete notebook as structured output. The model produces a JSON object with the correct nbformat keys and a cells array that alternates between Markdown and code. Save that output as a .ipynb file and open it in Jupyter — it works.

This is qualitatively different from asking a model to generate a Word document or a PDF. Those formats require post-processing: the model generates content, a library formats it. With .ipynb, the model generates the file itself. The gap between what the model produces and what Jupyter reads is a single file save.

Practical patterns that emerge from this:

Scaffolding a new analysis

Describe the analysis you want to perform — the dataset, the question, the method — and ask for a notebook scaffold. The model generates the section headings as Markdown cells, placeholder code cells with the key operations, and brief prose in each Markdown cell explaining what the code block will do. You fill in the actual implementation. The structure is there before you write a line of real code.

Documenting existing notebooks

The reverse operation: hand a model a notebook that has code cells but sparse Markdown documentation, and ask it to generate the explanatory text. Because the model can see both the code and any existing prose, it can produce Markdown cells that accurately describe what each section does, note important assumptions, and flag anything that looks unusual. The documentation becomes a second pass rather than an afterthought.

Translating between languages

A notebook in Python can be translated to R or Julia at the code-cell level while keeping the Markdown documentation unchanged. The prose describes the analysis, not the implementation. This separation — Markdown for what, code for how — makes language-level translation more tractable than it would be in a monolithic script.

Where the format shows its seams

None of this means .ipynb is a clean format. It was designed for interactivity and rendering, not portability or long-term archiving. A few problems are worth knowing about.

It is not standard Markdown

The documentation cells contain Markdown, but the file is not a Markdown file. You cannot open a .ipynb in a Markdown editor and read formatted prose. You cannot version-control a notebook meaningfully with standard text-diff tools because the JSON structure mixes source, output, and execution metadata in the same file. A change to one output cell produces a large diff that obscures any change to the actual content. Tools like nbstripout exist specifically to strip outputs before committing, restoring some diffability.

Execution order is not guaranteed

A notebook is not a script. Cells can be run in any order, and the execution counter in each cell records only when it was last run, not whether the cells are in a consistent state. A notebook where cells have been run out of order, or where earlier cells have been edited without re-running later ones, may contain outputs that contradict the current source. This is a persistent source of confusion and unreproducible results in shared notebooks.

Outputs inflate file size

Stored outputs — especially plots and images, which are embedded as base64-encoded strings — make .ipynb files large and opaque. A notebook with several matplotlib figures can exceed several megabytes before any code has been changed. The base64 blobs are not Markdown and not human-readable, which partly undermines the format's otherwise transparent structure.

The format has versioned

The .ipynb format has gone through multiple versions (the current spec is nbformat 4, introduced in 2014). Older notebooks in nbformat 3 have a slightly different structure — notably, they use a "worksheets" layer that was removed in version 4. Most tools handle both, but the version field at the top of every notebook file exists for a reason.

Despite these rough edges, the format has held. The combination of human-readable documentation, executable code, and captured output in a single portable file is genuinely useful — and genuinely rare. The decision to store documentation as raw Markdown, rather than as rendered HTML or some proprietary encoding, was the right one. It keeps the notebooks legible to humans editing them in a text editor, to version control systems tracking them in a repository, and to language models reading them as training data or generating them as output.

That last use was not anticipated when the format was designed. It is one of those cases where a decision made for one reason turns out, years later, to have been the right decision for a reason nobody had yet.

Further reading: Markdown as RAG — your own lightweight context layer · Math in Markdown · Mermaid diagrams — the format AI actually draws in