nbconvert
nbconvert turns a .ipynb file into something that is not a notebook: HTML, LaTeX, PDF, Markdown, reStructuredText, AsciiDoc, a Reveal.js slide deck, or a plain executable script. It is the machinery behind the Download As menu in Jupyter, and it is what runs when you export a notebook from the command line with jupyter nbconvert --to html report.ipynb. Internally the pipeline is three stages: preprocessors that mutate the notebook object (execute cells, clear outputs, drop cells with a given tag), an exporter that feeds the notebook through a Jinja template, and a writer that puts the result on disk along with any extracted images. Every stage is a traitlets-configurable class, so anything the CLI does you can also do from Python or override with your own subclass. It can also execute a notebook and save the results back, which is how most teams use it as a regression test that their notebooks still run.
The standard and effectively unavoidable way to get anything out of a .ipynb file, and the execute-in-CI use case alone justifies it. Reach for a narrower tool when your need is narrow: nbclient or papermill for running notebooks, nbstripout for cleaning them, and a site generator when you are publishing more than one.
Use it if
- You need a CI check that your notebooks still execute end to end, and you want the failure to be a nonzero exit code rather than someone noticing next quarter
- You publish notebook output to people who do not use Jupyter: a nightly HTML report on internal storage, a Markdown page in a docs site, or a slide deck for a review meeting
- You want notebooks out of your git diffs: converting to a script or clearing outputs in a pre-commit hook makes review possible again
- You need programmatic control over what appears in the output: hide input cells for a stakeholder report, drop cells tagged as setup, or run a template you wrote
- You already have the Jupyter stack installed, so nbconvert is almost free and the alternative is writing your own nbformat walker
- Execution is all you want. nbclient is the library nbconvert calls to run cells, and papermill adds parameter injection and per-run output notebooks; either is a smaller dependency than the full export pipeline
- You are building a documentation site or a book from notebooks. Jupyter Book, nbsphinx, or Quarto give you cross-references, navigation, and build caching, none of which nbconvert has because it converts one file at a time with no idea a second file exists
- You need PDF and cannot install system packages. The --to pdf path wants a working XeLaTeX plus pandoc, which is a large layer in a container image, and --to webpdf wants playwright plus a Chromium download. Neither is solved by pip alone
- You are converting notebooks you did not write and serving the HTML. Output is not sanitized unless you pass --sanitize-html, so a notebook can inject script into your page, and 7.17.1 was a security release for two CVEs (CVE-2026-39377 and CVE-2026-39378)
- You only want to strip outputs before committing. nbstripout is a single-purpose tool with a pre-commit hook and a git filter, and it does not drag in beautifulsoup4, bleach, mistune, pandocfilters, and jinja2 for that one job
- You want output you can style without learning Jinja. Customizing beyond the built-in templates means the nbconvert 6+ template directory layout plus traitlets configuration, and the old 5.x .tpl files only work through --template-file in compatibility mode
- You need active feature development. There are 563 open issues (597 counting PRs), a volunteer team, and roughly a year between 7.16.6 in January 2025 and 7.17.0 in January 2026; fixes land, new capability rarely does
Setup reality
pip install nbconvert is the easy part, and it is not small: beautifulsoup4, bleach, defusedxml, jinja2, jupyter-core, jupyterlab-pygments, markupsafe, mistune, nbclient, nbformat, packaging, pandocfilters, pygments, and traitlets all come along. Everything past HTML and Markdown needs software pip cannot install. LaTeX, reStructuredText, and AsciiDoc output go through pandoc, which you install from your OS package manager. --to pdf additionally needs a TeX distribution with XeLaTeX and the recommended font packages, which on Debian means texlive-xetex, texlive-fonts-recommended, and texlive-plain-generic. --to webpdf skips TeX by rendering HTML in headless Chromium, but needs pip install 'nbconvert[webpdf]' and then a browser download, with --allow-chromium-download to let playwright fetch one. The qtpdf and qtpng exporters need pyqtwebengine. Executing notebooks needs a kernel: nbconvert does not depend on ipykernel, so a fresh install will fail with a kernel-not-found error until you install it yourself. Two more sharp edges: --to is mandatory since 6.0 removed the HTML default, and the readthedocs install page still claims Python 3.9 to 3.12 while the package metadata allows anything from 3.9 up.
Patterns
Convert a notebook to a standalone HTML filehtml-export
jupyter nbconvert --to html report.ipynb
jupyter nbconvert --to html --template classic report.ipynb
jupyter nbconvert --to html --template lab --theme dark report.ipynb
jupyter nbconvert --to html --embed-images report.ipynb--to is required; 6.0 removed the old implicit HTML default, so a bare jupyter nbconvert now errors. Without --embed-images, plots land in a report_files/ directory next to the HTML and the page breaks the moment you move one file without the other.
Run every notebook and fail the build if one breaksexecute-in-ci
jupyter nbconvert --to notebook --execute --inplace notebooks/*.ipynb
# keep going and record the traceback in the cell output instead of aborting
jupyter nbconvert --to notebook --execute --allow-errors nb.ipynb
# the default per-cell timeout is 30 seconds
jupyter nbconvert --to notebook --execute \
--ExecutePreprocessor.timeout=600 \
--ExecutePreprocessor.kernel_name=python3 nb.ipynbWithout --inplace the result is written to nb.nbconvert.ipynb rather than over the original. The 30 second default timeout is the usual cause of a CI failure that does not reproduce locally; set it to None or -1 to remove the limit entirely. nbconvert does not depend on ipykernel, so install it or every run dies on kernel lookup.
Extract the code as a runnable scriptnotebook-to-script
jupyter nbconvert --to script analysis.ipynb # writes analysis.py
jupyter nbconvert --to script --stdout analysis.ipynb
jupyter nbconvert --to script model.ipynb --output-dir src/The output extension follows the kernel, so a Julia notebook gives you a .jl file. IPython magics such as %matplotlib inline are emitted as get_ipython() calls, which means the script only runs under IPython unless you strip them first.
Strip outputs before committingclear-outputs
jupyter nbconvert --clear-output --inplace notebooks/*.ipynb
# same thing spelled out
jupyter nbconvert --to notebook --inplace \
--ClearOutputPreprocessor.enabled=True notebook.ipynb--clear-output is a shortcut flag that already implies notebook output and in-place writing. It removes outputs and execution counts but leaves cell metadata alone, so tags and slide settings survive. If this is all you want from nbconvert, nbstripout does it with far fewer dependencies.
Produce a report with no code visiblehide-code-for-reports
jupyter nbconvert --to html --no-input report.ipynb
# keep code, drop the In[ ] / Out[ ] prompts
jupyter nbconvert --to html --no-prompt report.ipynb--no-input hides input cells and both prompt gutters, which is what you want for a stakeholder-facing page. It hides the code, it does not remove it from anywhere else, and markdown cells still render, so do not use it to conceal credentials that are written in a cell.
Drop cells by tag instead of by handremove-tagged-cells
jupyter nbconvert report.ipynb --to html \
--TagRemovePreprocessor.enabled=True \
--TagRemovePreprocessor.remove_cell_tags remove_cell \
--TagRemovePreprocessor.remove_input_tags hide_code \
--TagRemovePreprocessor.remove_all_outputs_tags hide_outputTags live in each cell's metadata and are editable from the JupyterLab property inspector. TagRemovePreprocessor is registered but disabled by default, so enabled=True is not optional. This is the clean way to keep scratch and setup cells in the notebook but out of the published version.
Pick between the two PDF pathspdf-output
# route 1: LaTeX. needs pandoc + a TeX distribution with xelatex
jupyter nbconvert --to pdf thesis.ipynb --template report
# route 2: headless Chromium. needs pip install "nbconvert[webpdf]"
jupyter nbconvert --to webpdf --allow-chromium-download report.ipynb
# route 3: stop at .tex and run xelatex yourself
jupyter nbconvert --to latex thesis.ipynb--to pdf gives typeset output and chokes on emoji, unusual fonts, and raw HTML in markdown cells. --to webpdf renders exactly what the HTML export looks like and handles anything a browser handles, at the cost of a Chromium download. Inside a container you may also need --disable-chromium-sandbox.
Export from Python instead of the CLIpython-api-export
from nbconvert import HTMLExporter
from nbconvert.writers import FilesWriter
exporter = HTMLExporter(template_name="classic")
body, resources = exporter.from_filename("report.ipynb")
FilesWriter(build_directory="out").write(body, resources, notebook_name="report")from_filename returns a two-tuple: the rendered text and a resources dict holding extracted images and metadata. Writing body to a file yourself and ignoring resources is the usual reason images go missing from the output.
Execute a notebook with the Python APIexecute-from-python
import nbformat
from nbconvert.preprocessors import ExecutePreprocessor
with open("pipeline.ipynb") as f:
nb = nbformat.read(f, as_version=4)
ep = ExecutePreprocessor(timeout=600, kernel_name="python3")
ep.preprocess(nb, {"metadata": {"path": "notebooks/"}})
with open("executed.ipynb", "w", encoding="utf-8") as f:
nbformat.write(nb, f)The path in the resources dict is the working directory the kernel runs in, so relative data paths inside the notebook resolve against it rather than against your script. preprocess mutates nb in place and returns it. A failing cell raises CellExecutionError unless you set allow_errors=True.
Render a notebook you did not writesanitize-untrusted-html
jupyter nbconvert --to html --sanitize-html untrusted.ipynbOff by default. Without it, HTML and script tags inside markdown cells and cell outputs pass straight into your page, which is a stored XSS bug the moment you serve it. Sanitization is not a substitute for keeping nbconvert current: 7.17.1 was released specifically to fix two CVEs.
Convert many notebooks into one output directorybatch-and-output-dir
jupyter nbconvert --to html --output-dir build/ notebooks/*.ipynb
jupyter nbconvert --to markdown notebook.ipynb --stdout
cat notebook.ipynb | jupyter nbconvert --stdin --to htmlEach notebook is converted independently with no shared state, so links between notebooks do not become links between outputs. Supporting files go to build/<name>_files/. --stdout only makes sense for single-file formats, since anything producing images has nowhere to put them.
Turn a notebook into a slide deckreveal-slides
jupyter nbconvert --to slides talk.ipynb
# serve locally so speaker notes and timers work
jupyter nbconvert --to slides talk.ipynb \
--reveal-prefix reveal.js --post serveCells need a slide type in their metadata (Slide, Sub-Slide, Fragment, Skip, Notes), set from the slideshow section of the JupyterLab property inspector. Untagged cells all pile onto the first slide. By default the deck loads reveal.js from a CDN, so an offline machine renders it unstyled.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| papermill | PyPI | You want to run a notebook with different parameters on a schedule and keep each executed copy, rather than convert it to another format. |
| nbclient | PyPI | You only need to execute a notebook programmatically; this is the library nbconvert itself uses, without the exporters and templates. |
| nbstripout | PyPI | The only thing you want is outputs removed before committing, via a git filter or a pre-commit hook. |
| jupyter-book | PyPI | You are publishing many notebooks as one site or book and need navigation, cross-references, and build caching. |