100% Local · Free · No Sign-up

Convert Jupyter Notebooks (.ipynb) to Markdown

Convert ipynb to md without Python, nbconvert, or a running kernel. The notebook JSON is parsed against the nbformat schema in a Web Worker on your own device, and each cell is mapped to its Markdown equivalent — prose verbatim, code into language-tagged fences, outputs beneath the cell that produced them.
Convert Jupyter Notebooks (.ipynb) to MarkdownDrag & drop · .ipynb

Processing happens 100% in your browser — nothing is uploaded.

100% Local Processing — Your files never leave your device

How to convert a Jupyter notebook to Markdown

  1. Drop the file: drag your .ipynb onto the dropzone, or click to browse. Nothing is uploaded — the file is read locally.
  2. Local processing: the notebook JSON is parsed in a Web Worker, then walked cell by cell — markdown cells verbatim, code cells into fenced blocks, outputs rendered by MIME type.
  3. Copy or download: take the Markdown to your clipboard, or save it as a .md file ready for GitHub, Obsidian, Hugo or a docs repo.

Why Convert Jupyter to Markdown with MD Convert?

  • Cell-type dispatch, not text scraping

    Each entry in the notebook's cells array is handled by its cell_type. Markdown cells are emitted verbatim so headings, links and tables survive byte-for-byte; code cells become fenced blocks tagged with the kernel language read from metadata.language_info, so Python, Julia and R notebooks all highlight correctly.

  • MIME-aware output rendering

    Stream output becomes a fenced block under its cell, image/png results are inlined as base64 data URIs so Matplotlib figures travel with the file, and text/plain is used for DataFrame and array reprs. Error outputs are kept with ANSI colour escape codes stripped from the traceback.

  • LaTeX and math passed through untouched

    Inline $...$ and display $$...$$ expressions are never rewritten, because math is not a Markdown construct and rewriting it would guess at your renderer. The output works with whatever math pipeline you already use in Obsidian, a static site generator or a docs platform.

  • Safe for unpublished research and client notebooks

    Parsing runs entirely in your browser, so a notebook holding customer data, an unredacted API key in a cell, or a pre-publication result never crosses the network. There is no upload, no queue, and no account.

Jupyter to Markdown: Before and After

Two cells of raw nbformat JSON and the Markdown they produce. Note that `source` is a line array, the fence language comes from the kernel metadata, and the LaTeX in the prose cell is passed through untouched.

Input · ipynb (nbformat JSON)
{"cells": [
  {"cell_type": "markdown", "source": ["## Gradient check\n", "Loss is $\\mathcal{L} = \\sum_i (y_i - \\hat{y}_i)^2$.\n"]},
  {"cell_type": "code", "execution_count": 3,
   "source": ["print(loss(y, y_hat))\n"],
   "outputs": [{"output_type": "stream", "text": ["0.0421\n"]}]}
], "metadata": {"language_info": {"name": "python"}}}
Output · Markdown
## Gradient check

Loss is $\mathcal{L} = \sum_i (y_i - \hat{y}_i)^2$.

```python
print(loss(y, y_hat))
```

```
0.0421
```

Understanding the Jupyter Format: nbformat 4

Format
Jupyter Notebook Format (nbformat)
Specification
nbformat 4
Media type
application/x-ipynb+json

An `.ipynb` file is not a document format in the way DOCX or PDF are — it is a single JSON object validated against the nbformat schema. At the top level it carries `nbformat`, `nbformat_minor`, a `metadata` object, and a `cells` array. Every entry in that array is a cell with a `cell_type` discriminator of `markdown`, `code` or `raw`, plus a `source` field that is almost always an array of strings with the newline retained at the end of each line.

Because the structure is schema-defined rather than inferred, conversion is a typed walk rather than a guess. A `markdown` cell already contains Markdown, so it is emitted verbatim. A `code` cell becomes a fenced block, and the language tag is read from `metadata.language_info.name`, falling back to `metadata.kernelspec.language` and finally to `python` — so a Julia or R notebook is tagged `julia` or `r`, not mislabelled.

Code cells also carry an `outputs` array, and that is where most of the format's complexity lives. Each output has an `output_type` — `stream` for stdout/stderr, `execute_result` and `display_data` for rich results keyed by MIME type, and `error` for exceptions with an ANSI-coloured `traceback`. Handling these correctly is the difference between documentation that shows what the code produced and a wall of raw JSON.

How cell types map to Markdown blocks

The walk over `cells` is exhaustive and order-preserving, so the narrative structure of the notebook survives exactly: prose, then the code it introduces, then what that code printed. Markdown cells are emitted with no transformation at all — their `source` already is Markdown, so re-parsing it would only risk corrupting it. Headings, links, tables, footnotes, HTML fragments and math all pass straight through.

Code cells are wrapped in a triple-backtick fence tagged with the notebook's kernel language, which is what gives you syntax highlighting on GitHub, in Obsidian and in any renderer that honours the info string. Empty cells — the trailing blank cell almost every notebook accumulates — are skipped rather than emitted as empty fences, because a stray ` ``` ``` ` pair renders as an empty grey box.

`raw` cells are not emitted. A raw cell exists to pass content through to a specific nbconvert target (LaTeX preamble, reveal.js directives), and its content is meaningless outside that pipeline. If you use raw cells to hold YAML frontmatter, paste it back at the top of the output manually.

Outputs: streams, results, images and tracebacks

Output rendering is opt-in-by-default and MIME-aware. A `stream` output — anything your code sent to stdout or stderr — becomes an untagged fenced block directly beneath the cell that produced it, so `print()` results and progress logs stay attached to their source.

For `execute_result` and `display_data` outputs, the `data` dictionary is inspected by MIME type. An `image/png` entry is emitted as a Markdown image with the base64 payload inlined as a `data:` URI, which means a Matplotlib figure survives into the Markdown as a real, self-contained image — no sidecar files, no broken relative paths when you move the file. If there is no PNG, the `text/plain` representation is used instead, which is what preserves the repr of a pandas DataFrame or a NumPy array.

`error` outputs are kept, because a traceback is often the most useful line in a notebook. The `traceback` frames are joined and stripped of ANSI colour escape codes — the `\u001b[0;31m` sequences that make a raw traceback unreadable in a plain-text renderer — leaving a clean fenced block.

Math, LaTeX and why it is passed through verbatim

Inline `$...$` and display `$$...$$` math is left exactly as authored. This is deliberate: math is not a Markdown construct, so any converter that tries to transform it is guessing at your renderer. Passing it through unchanged means the output works with whichever pipeline you already use — MathML-based renderers, a LaTeX-aware static site generator, Obsidian's built-in math, or a docs platform with math enabled.

The one thing to watch is escaping. Some renderers treat a backslash-heavy expression inside a table cell or a footnote differently, and a few Markdown flavours require math to be enabled explicitly. If an expression renders as literal dollar signs, the problem is the renderer's math extension, not the conversion — the source text is byte-identical to what was in the notebook cell.

Why this is not nbconvert, and when that matters

`jupyter nbconvert --to markdown` is the canonical tool and it does more than this converter does: it can execute the notebook first, apply templates, and write images out as separate files in a companion directory. It also requires a Python environment with Jupyter installed, and it runs wherever that environment lives.

This converter targets the other half of the problem — the notebook you were sent, the notebook on a machine with no Python, the notebook you are not allowed to upload. Parsing happens in a Web Worker on your own device, so a notebook containing client data, an unredacted API key in a cell, or unpublished research never crosses the network. You can confirm that in the Network panel of DevTools while you convert: there is no request to confirm.

Known limitations of Jupyter to Markdown conversion

Being explicit about what a converter cannot do saves you a wasted upload. These are the boundaries of what is recoverable from Jupyter programmatically:

  • Raw cells are skipped, because their content is only meaningful to a specific nbconvert target.
  • Widget state and interactive outputs (ipywidgets, Plotly, Bokeh) are not recoverable — they need a live kernel and a JavaScript runtime, neither of which exists in a static Markdown file.
  • Only `image/png` outputs are inlined. Other rich MIME types fall back to their `text/plain` representation, so an SVG-only figure is not embedded.
  • Execution counts, cell IDs and per-cell metadata (tags, `collapsed`, slide directives) are dropped — they have no Markdown equivalent.
  • Attachments referenced as `attachment:name.png` inside Markdown cells are left as-is, so the link will not resolve outside the notebook.
  • A very large notebook of inlined PNG figures produces a correspondingly large Markdown file, since every image is carried as base64 text rather than a file reference.

Who Converts Jupyter to Markdown?

  • Data Scientists

    Publishing an analysis as a README or a wiki page so reviewers can read the narrative and the code without launching a kernel or trusting a rendered HTML export.

  • ML Engineers

    Flattening experiment notebooks into plain Markdown for pull-request review, where a JSON diff of cell metadata and execution counts is unreadable.

  • RAG & LLM builders

    Turning a notebook corpus into clean text chunks for embedding, without feeding base64 blobs, execution counts and widget state into the tokeniser.

  • Educators & Technical Writers

    Converting course notebooks into course notes for a static site or Obsidian vault, keeping prose, code and math in the order the lesson was authored.

How You Can Verify the Privacy Claim

Browser-only

Zero server upload

Conversion runs inside your browser tab. Open DevTools, switch to the Network panel, and convert a file: for every format except URL to Markdown you will see no request carrying your document — because there is no endpoint to send it to.

Web Worker

Off the main thread

Heavy parsing is dispatched to a Web Worker, so a 500-page PDF or a large spreadsheet never freezes the interface. Everything is plain JavaScript — no native plugin, no WebAssembly toolchain, nothing to install.

No account

Nothing to sign up for

No login, no quota, no paywall, and no tracking tied to your files. Analytics are cookieless and aggregate only. Read the privacy policy for the full data-flow breakdown, including the one proxied exception.

The parsers doing the work

No proprietary black box: each format is handled by a widely audited open-source library, running client-side at the version pinned in our lockfile.

Jupyter to Markdown — FAQ

Are code cell outputs and plots included in the Markdown?

Yes. Stream output (anything printed to stdout or stderr) becomes a fenced block beneath its cell, and image/png outputs are inlined as base64 data URIs so a Matplotlib figure remains visible in the Markdown with no sidecar image files. Outputs with no PNG fall back to their text/plain representation, which is what preserves a pandas DataFrame repr.

How is this different from jupyter nbconvert --to markdown?

nbconvert is the canonical tool and does more: it can execute the notebook first, apply templates, and write images to a companion directory. It also needs a Python environment with Jupyter installed. This converter needs a browser, works on a machine with no Python, and never transmits the notebook — which matters when you are not permitted to upload it.

Does LaTeX and math survive the conversion?

Yes, verbatim. Inline $...$ and display $$...$$ expressions are copied through unchanged. If math renders as literal dollar signs in your target, the math extension is disabled in that renderer — the source text is byte-identical to the notebook cell.

What happens to raw cells, widgets and interactive plots?

Raw cells are skipped, because their content only means something to a specific nbconvert target such as a LaTeX preamble. Widget state and interactive output from ipywidgets, Plotly or Bokeh cannot be recovered either — they require a live kernel and a JavaScript runtime that a static .md file does not have.

Is the output ready for Obsidian, JupyterLab docs or a static site?

Yes. The output is GitHub Flavored Markdown with ATX headings, fenced code blocks and standard image syntax, so it renders in Obsidian, GitHub, GitLab and any Jekyll or Hugo pipeline. If your site generator needs YAML frontmatter, add it at the top of the file — notebook metadata is not converted into frontmatter.

In-Depth Jupyter Guides