What an .ipynb file actually contains
A notebook is JSON: cells, saved outputs, and metadata. What is stored, what is only a recording of a past run, and what never makes it into the file.
Updated
An .ipynb file is not a compiled program and it is not a Word document with a strange extension. It is a JSON document that describes an ordered list of cells, plus a small amount of notebook-level metadata. Jupyter Notebook, JupyterLab, VS Code, PyCharm, and Google Colab all read and write that same JSON shape. If you rename a notebook to .json and open it in any text editor, you are looking at the entire notebook. Nothing important is hiding in a binary blob beside it.
That fact is the whole reason notebook tools can exist without a Python kernel. A converter, a viewer, a diff, or a repair utility can read the file as data. It cannot recover variables that were only alive in memory when someone last pressed Shift+Enter. Those variables were never written into the file.
This guide walks through the pieces you will actually see, using the public sample notebook as the concrete file. Download it and open it beside this page if you want to check each claim against the bytes.
The top-level object
A current notebook starts as one JSON object with four fields that matter:
nbformatis the major format version. For notebooks written in the last several years this is4.nbformat_minoris a smaller revision. You will see values such as4or5. Minor bumps add optional fields. They do not change the idea of cells and outputs.metadatais a free-form object for the notebook as a whole. Kernelspec and language info usually live here. Tools may add their own keys. Unknown keys are supposed to be preserved, not treated as errors.cellsis the array you care about. Order in this array is the order of the notebook. There is no separate “page” or “section” structure. Headings are just Markdown inside cells.
If nbformat is missing, or cells is not an array, the file is not a notebook yet. It might be a fragment someone copied out of a notebook, a pretty-printed subset, or JSON that was truncated while saving. A repair tool can sometimes rebuild a valid shell around damaged JSON. It cannot invent cells that were never saved.
Cells
Each cell is an object. The field cell_type says which kind it is.
Markdown cells hold prose. The source is the Markdown the author typed: headings, lists, links, math delimiters, images referenced by URL. A Markdown cell has no outputs and no execution_count. Nothing “runs.” When a notebook UI renders the cell, it is only formatting text that is already in source.
Code cells hold source in a programming language, almost always Python, plus optional saved outputs from a previous run. The language is not stored on every cell. It is implied by the notebook metadata. A code cell can be unrun. In that case execution_count is null and outputs is an empty array. That is a normal notebook, not a broken one.
Raw cells are uncommon. They store text that notebook frontends are not supposed to interpret as Markdown or as code. Export tools often pass them through as plain text or drop them. If you have never created one on purpose, you can ignore them until a file actually contains cell_type: "raw".
The source field is usually an array of strings, one fragment per line, with the newline kept at the end of each fragment except sometimes the last. It can also be a single string. Both are legal. A tool that assumes only one shape will mishandle the other. When you read a notebook by eye, join those fragments in order and you have the cell as the author saw it.
Cells may also have an id string. Newer notebooks use ids so that interfaces can track a cell when it moves. Older notebooks omit ids. Absence of an id is not corruption.
Outputs are a recording, not a live result
A code cell’s outputs array is a log of what the kernel sent back the last time that cell ran, if it ran. Opening the file does not execute it. If you change the source and do not run the cell, the old output stays until something strips it or the cell is run again. That mismatch is the source of a lot of confusion in grading, code review, and “the PDF doesn’t match the code.”
Common output objects:
output_type: "stream"is text from stdout or stderr. Thenamefield is"stdout"or"stderr". Thetextfield is a string or an array of strings. The sample notebook’s only output is a stdout stream with two lines: the title and the number4.output_type: "execute_result"is the value of the last expression when the kernel displays it, often with several MIME types underdata(text/plain, sometimestext/html).output_type: "display_data"is similar, but it comes from an explicit display, such as a plot. Image outputs usually appear as base64 text under a key likeimage/png. The image is inside the JSON, which is why notebooks with many plots become huge.output_type: "error"records an exception:ename,evalue, and atracebackarray. The traceback is text. It often contains file paths from the machine that ran the cell.
There is no general rule that every code cell has exactly one output. A cell can print several times, display a figure, and then raise. All of those objects sit in order in outputs.
execution_count is the number the UI showed in the bracket at the left, such as [1]. It increases as you run cells during a session. It is not a hash of the source. Two notebooks can have identical code and different counts because one was run more. Diff tools that compare raw JSON will flag every count change even when the code did not change.
Metadata is a hint
metadata.kernelspec names the kernel the author intended: a display name, a language, and a kernel name such as python3. That block does not contain the Python executable, the installed packages, or the virtualenv path. Someone else’s machine can open the notebook and still fail to run it because the kernel named in metadata is not installed there.
metadata.language_info often includes the language name and sometimes a version string. Treat the version as documentation of what the author used, not as a lockfile. Package pins, if they exist at all, are usually in a separate requirements.txt or environment.yml that may or may not travel with the notebook.
Cell-level metadata holds editor details: collapsed flags, tags, slideshow slide types, widget state. Export tools that do not understand a tag will typically ignore it. Slideshow metadata does not turn a notebook into slides by itself. A slides export has to read those tags and build a presentation. If the tags were never set, every cell is just a cell.
Widget state and custom MIME bundles can be large and opaque. A viewer can show a placeholder when it does not know how to render a widget. It should not pretend the widget is still interactive. The interaction lived in the kernel and the frontend session, not in the static JSON.
What the file does not contain
People expect a notebook to “have the data” because they saw a dataframe on screen. Sometimes the dataframe was printed and the text is in an output. Sometimes only a summary was printed. Sometimes the data was a variable that never got written out. The file does not include:
- The Python process, its memory, or variables that were not displayed.
- Files the code read from disk, unless the author also embedded that content in an output.
- The package environment, beyond whatever the author pasted into a cell or into metadata.
- Credentials that were only present in the shell environment. If a secret was printed or hardcoded in
source, it is in the file. If it was only in an environment variable and never displayed, it is not.
Attachments are a separate, less common place for images referenced from Markdown. Many notebooks never use attachments and instead put images only in code outputs. Looking only at outputs and missing attachments (or the reverse) is how an export quietly drops a figure.
How to inspect a notebook without Jupyter
- Download the sample notebook.
- Open it in a text editor. You should see
nbformat, acellsarray of length 3, and one stream output. - Join the
sourcearrays in your head or with a formatter. The code cell is three lines. The output text is two lines. They match because that cell was run and then saved. - Change nothing, close the editor, and open the same file in a notebook UI if you have one. The rendered page is a view of this JSON, not a second copy of the work.
If a file will not open, the next question is whether the JSON parses. A missing comma, a truncated download, or a notebook saved only halfway will fail before any kernel starts. That is a file problem, not a Python problem.
Why this shape matters for exports
Every export format is a decision about which of these fields to keep. A Python script can take code-cell source and ignore outputs. A PDF can typeset Markdown, code, and whatever images happen to be stored. A JSON pretty-printer can show the whole document and change almost nothing. None of those tools re-run the notebook unless they explicitly claim to, and a browser converter that never starts a kernel cannot re-run it.
If you only remember one check before you share a notebook, make it this: open the file as JSON, or open it in a viewer, and confirm the outputs on the page are the outputs you mean to publish. The source you are looking at in an editor may be newer than the recording stored beside it.
The sample notebook is the smallest file that still has all of the usual pieces: Markdown, code, a saved stream, and kernel metadata. The guide on what exports keep and drop uses that same file to show how those pieces survive a conversion. The guide on reading a notebook without Jupyter covers viewers and text editors as ways to inspect a file you do not want to execute.

