Markdown to reStructuredText: A Migration Guide for Sphinx and Read the Docs
Convert Markdown to reStructuredText for Sphinx and Read the Docs. Learn how RST headings, links, and code spans differ, with a worked example and migration tips.
Markdown to reStructuredText: A Migration Guide for Sphinx and Read the Docs
If you write Python, you have almost certainly read documentation built with Sphinx. The official CPython docs, the NumPy and Django manuals, and most of the projects hosted on Read the Docs all share one trait: their source files end in .rst, not .md. That extension stands for reStructuredText, and the day you need to move a Markdown README into one of those doc trees is the day you discover the two formats look similar but behave differently in ways that quietly break the rendered page.
This guide walks through what actually changes when you go from Markdown to reStructuredText, shows a worked conversion, and explains why a few of the differences trip people up every single time.
Why Sphinx picked reStructuredText in the first place
Markdown was designed to be readable as plain text and to produce simple HTML. reStructuredText was designed for technical documentation that spans hundreds of pages. The difference shows up in features Markdown never had: directives like .. code-block:: and .. toctree::, an interpreted-text role system for cross references, and substitution definitions you can reuse across a whole book.
Sphinx leans on all of that. A :func: role can link to an API entry by name. A .. toctree:: directive builds the entire navigation sidebar from a list of file paths. None of that has a clean Markdown equivalent, which is why large doc projects standardized on RST years ago and why Read the Docs renders it natively. You can author in Markdown with the MyST parser, but the moment you join an existing RST-based repo, you are converting.
The differences that actually bite
Three constructs change in ways that are easy to get wrong by hand.
Headings use an underline, not a hash. This is the single biggest surprise. RST has no # marker. You write the title text on one line and then a row of a single punctuation character on the next line. The underline has to be at least as wide as the title, or Sphinx emits a warning and may drop the heading entirely. A concrete rule worth memorizing: the punctuation character you pick defines the heading level by the order it first appears in the document. There is no fixed mapping, so a project might use = for the top level and - for the second, while another flips them.
Inline code needs two backticks. Markdown wraps a code span in one backtick: ` value . In RST a single backtick is an interpreted-text role, not literal code. Paste a Markdown code span straight into a .rst file and the text silently changes meaning. Literal code in RST takes double backticks: ` value ``. This is the failure that produces the most confusing rendered output, because the page builds without error and just looks wrong.
Links flip inside out. Markdown writes [docs](https://example.com). RST writes the same link as a phrase reference: a backtick, the link text, a space, the URL in angle brackets, a closing backtick, then a trailing underscore. The result is ` docs <https://example.com>_ `. Both the angle brackets and that final underscore are required, and the underscore is the part people forget every time.
Fenced code blocks become a .. code-block:: directive with the body indented three spaces, and a standalone  image becomes a .. image:: directive with an :alt: option line. Bold and italic, happily, are identical in both formats — RST already uses **bold** and *italic* — so those carry over untouched.
A worked example
Here is a small chunk of Markdown:
# Setup
Install with `pip install toolora` and read the [docs](https://toolora.info).
Run it through the Markdown to reStructuredText converter and you get:
Setup
=====
Install with ``pip install toolora`` and read the `docs <https://toolora.info>`_.
Look at the heading underline: Setup is five characters, so it gets exactly five equals signs. The inline ` pip install toolora ` became double-backtick literal code. The Markdown link became a phrase reference with angle brackets and the trailing underscore. Three transformations, all in one paste, none of which you had to remember.
How I migrated a README without losing an afternoon
I had a side project whose README had drifted into a real getting-started guide — install steps, a configuration table, a troubleshooting section — and the docs site was a fresh Sphinx build expecting RST. My first instinct was to hand-edit, and I got about four headings deep before I miscounted an underline and Sphinx warned that a section title was shorter than its underline. I deleted my work and pasted the whole README into the converter instead.
The output dropped straight into docs/index.rst and built clean on the first try. What surprised me was the links. The README had eleven of them, and every single ` text <url>_ came out with the trailing underscore in place — the exact character I would have fumbled by hand on at least a couple of them. The code fences turned into .. code-block::` directives with the right three-space indent, which is the other thing I always get subtly wrong. The whole migration took the length of one coffee instead of an afternoon of underline-counting.
Tips for a smooth migration
A few things make the move painless:
- Convert section by section for large files. If a page is thousands of lines, paste it in chunks so you can eyeball each block of output before committing. The converter handles a full document fine, but reviewing in pieces catches any unusual Markdown your team invented.
- Check your heading character convention. Because RST levels are defined by order of appearance, decide the project convention up front (
=for<h1>,-for<h2>,~for<h3>is a common one) and keep every file consistent so the table of contents nests correctly. - Mind the round trip. If you ever need to pull RST or HTML back into Markdown for a blog or a GitHub wiki, the reverse path exists too — the HTML to Markdown converter handles the other direction when you are publishing rendered docs somewhere Markdown is the native format.
- Keep the raw Markdown. Authors usually think faster in Markdown. Draft there, convert to RST at commit time, and you keep both the authoring speed and the toolchain Sphinx expects.
Where to go from here
The gap between Markdown and reStructuredText is small in spirit and fiddly in practice. Headings, inline code, and links are the three places a careless paste breaks the build or, worse, renders quietly wrong. Once you let a converter handle the mechanical rewriting, the only real decision left is your heading-character convention — and that is a one-time choice per project.
If your docs live in Sphinx or on Read the Docs, drafting in Markdown and shipping RST is a genuinely comfortable workflow. The format your fingers prefer and the format your build expects no longer have to be the same one.
Made by Toolora · Updated 2026-06-13