Exported source
def _read_json(self, encoding=None, errors=None):
return loads(Path(self).read_text(encoding=encoding, errors=errors))A notebook is just a json file.
It contains two sections, the metadata…:
…and, more importantly, the cells:
[{'cell_type': 'markdown',
'id': '801558df',
'metadata': {},
'source': ['## A minimal notebook']},
{'cell_type': 'code',
'execution_count': None,
'id': 'e2147a69',
'metadata': {'time_run': '2026-01-04T20:52:49.901559+00:00'},
'outputs': [{'data': {'text/plain': ['2']},
'execution_count': 0,
'metadata': {},
'output_type': 'execute_result'}],
'source': ['# Do some arithmetic\n', '1+1']}]
The second cell here is a code cell, however it contains no outputs, because it hasn’t been executed yet. To execute a notebook, we first need to convert it into a format suitable for nbclient (which expects some dict keys to be available as attrs, and some available as regular dict keys). Normally, nbformat is used for this step, but it’s rather slow and inflexible, so we’ll write our own function based on fastcore’s handy dict2obj, which makes all keys available as both attrs and keys.
dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
We use an AttrDict subclass which has some basic functionality for accessing notebook cells.
Convert dict js to an AttrDict,
We can now convert our JSON into this nbclient-compatible format, which pretty prints the source code of cells in notebooks.
The abstract syntax tree of source code cells is available in the parsed_ property:
Return notebook at path
This reads the JSON for the file at path and converts it with dict2nb. For instance:
"{'cell_type': 'markdown', 'id': '801558df', 'metadata': {}, 'source': '## A minimal notebook', 'idx_': 0}"
The file name read is stored in path_:
Create an NbCell containing text
Returns an empty new notebook
Use this function when creating a new notebook. Useful for when you don’t want to create a notebook on disk first and then read it.
Convert parsed notebook to dict
This returns the exact same dict as is read from the notebook JSON.
Convert nb to a str
To save a notebook we first need to convert it to a str:
Write nb to path
This returns the exact same string as saved by Jupyter.
Here’s how to put all the pieces of fastcore.nbio together:
Call self as a function.
preferred_out selects the best MIME type from an output’s data dict, preferring HTML by default:
(('text/html', ['<b>42</b>']), ('text/html', ['<b>42</b>']))
Apply nd o text, returning processed result
Helper to create an error output dict
Helper to create a display_data output dict
Helper to create an execute_result output dict
Helper to create an output stream dict
Concatenate stream outputs by name (stdout/stderr), preserving execute_result at end
concat_streams merges consecutive stream outputs by name and moves execute_results to the end, like standard jupyter output rendering:
[{'output_type': 'execute_result',
'data': {'text/plain': ['42']},
'metadata': {}},
{'output_type': 'stream', 'name': 'stdout', 'text': 'hello '},
{'output_type': 'stream', 'name': 'stdout', 'text': 'world\n'},
{'output_type': 'stream', 'name': 'stderr', 'text': 'warn\n'}]
[{'output_type': 'stream', 'name': 'stdout', 'text': 'hello world\n'},
{'output_type': 'stream', 'name': 'stderr', 'text': 'warn\n'},
{'output_type': 'execute_result',
'data': {'text/plain': ['42']},
'metadata': {}}]
Convert a single output dict to an HTML string
<pre class="!border-0 !rounded-none !my-0 !p-0"><code class="nohighlight">42</code></pre>
Render a full list of outputs, concatenating streams first.
<pre class="!border-0 !rounded-none !my-0 !p-0"><code class="nohighlight">hello world
</code></pre>
<pre class="!border-0 !rounded-none !my-0 !p-0"><code class="nohighlight">warn
</code></pre>
<pre class="!border-0 !rounded-none !my-0 !p-0"><code class="nohighlight">42</code></pre>
Render notebook outputs to concise text, using XML-ish tags when multiple outputs are present.
A single output renders as plain text directly:…
…but multiple outputs get wrapped in XML-ish tags so they stay distinguishable. For rich outputs we prefer markdown by default; pass html1st=True to prefer HTML instead. Outputs without a text representation (e.g. images alone) render as empty.
<stdout>
hello world
</stdout>
<stderr>
warn
</stderr>
<execute_result>
42
</execute_result>
Convert notebook cells to XML format
Convert NbCell to concise XML format
We can view any notebook as concise XML. For instance, our minimal notebook:
'<code id="b1d7a3b5"><source>print(1)<out>1\n</out></code>'
Read, query, and edit Jupyter notebooks
We can now open a notebook and access its metadata and cells:
(['solveit_dialog_mode', 'solveit_ver'], 2, 2)
Notebooks’ repr is their xml:
<nb path="/Users/jhoward/aai-ws/fastcore/tests/minimal.ipynb"><markdown id="801558df">## A minimal notebook</markdown><code id="e2147a69"><source># Do some arithmetic
1+1<out>2</out></code></nb>
You can also get a more concise version that doesn’t include outputs or the full path:
<nb path="minimal.ipynb"><markdown id="801558df">## A minimal notebook</markdown><code id="e2147a69"># Do some arithmetic
1+1</code></nb>
Cells can be accessed by integer index or by their string id:
You can directly set a cell’s source by id or index:
You can also update outputs and metadata directly on a cell:
[{'output_type': 'execute_result', 'data': {'text/plain': ['4']}}]
The add method inserts a new cell at a given position (defaulting to the end):
Add a new cell with source at idx (default: end), or after/before a cell id
(4, '## A minimal notebook')
Cells can also be inserted relative to an existing cell by id:
['# Before first', '## A minimal notebook', '# After first']
Add a new cell with source at idx (default: end), or after/before a cell id
md is a shortcut to add(..., cell_type='markdown')
You can delete by id or index:
The find method searches cell sources by regex, returning matching cells:
Find cells with source matching regex pat
[{'cell_type': 'code',
'execution_count': None,
'id': 'e2147a69',
'metadata': {'time_run': '2026-01-04T20:52:49.901559+00:00', 'custom': True},
'outputs': [{'output_type': 'execute_result',
'data': {'text/plain': ['4']}}],
'source': '2+2',
'idx_': 1}]
Move cells with src_ids after/before a cell id, or to end
Cells can be moved by id, either relative to another cell or to the end:
True
Use save to write to disk:
If no path is passed, the path used in open() will be re-used.
Show cell source with optional line numbers
The view method displays a cell’s source with optional line numbers: