# How to Format Python Code Blocks in Markdown Files


[Ruff](https://pydevtools.com/handbook/reference/ruff.md) 0.16.0 formats Python code blocks inside Markdown files, so the samples in a README or a docs page follow the same style as the rest of the project.

This happens by default. `ruff format .` now discovers `.md` files alongside `.py` files, which means the first format run after upgrading to 0.16 can rewrite documentation that has never been formatted before. Ruff 0.15 could format a Markdown file named explicitly under `--preview`, but never picked `.md` up when scanning a directory.

## Format a Markdown File

Pass the file to `ruff format` the same way as a Python file:

```bash
uv run ruff format README.md
```

Ruff reaches inside the recognized fences and reports `1 file reformatted`. Prose, headings, and non-Python blocks are untouched, so this:

````markdown
```python
x = {  "a":1,   "b":2 }
```
````

becomes `x = {"a": 1, "b": 2}`, still inside its fence.

Formatting a whole tree picks up Markdown automatically, so this reformats `.py` and `.md` together:

```bash
uv run ruff format .
```

The `line-length` setting in `pyproject.toml` applies to the code inside a block.

## Which Code Fences Does Ruff Format?

Ruff matches on the fence's info string.

| Info string | Formatted as |
|---|---|
| `python`, `py`, `python3`, `py3` | Regular Python source |
| `pyi` | Type stub |
| `pycon` | REPL session, including `>>>` prompts |

Quarto's curly form of any of those tags matches too, so `{python}` and `{pycon}` are formatted. MyST `{code-cell}` fences are not. Attributes after the tag still match, so `python title="example.py"` works, as do `~~~` fences.

Everything else is left alone, including fences with no info string and tags such as `bash`, `console`, and `ipython`. An unrecognized tag is skipped whether or not the code inside it is valid Python.

> [!NOTE]
> Ruff also skips any block it cannot parse or cannot safely reformat, reporting the file as unchanged and exiting 0 with no warning. An unchanged block is not proof that it was already formatted. A `>>>` session inside a `python` fence is the case most people hit; tag it `pycon` to have it formatted.

## Preview Changes Before Writing Them

Use `--diff` to print a unified diff without touching the file, or `--check` to fail without writing:

```bash
uv run ruff format --diff README.md
uv run ruff format --check .
```

Both exit 1 when a file needs reformatting, which makes either one usable as a CI gate. In 0.16, `--check` prints an annotated diff for each file that would change.

> [!WARNING]
> `ruff check` does not lint Markdown. Lint rules apply only to `.py` files; the formatter is the only half that reads your code blocks. A green `ruff check` therefore says nothing about the code in your docs, and in a project that has `.py` files there is no warning to tip you off.

## Exclude Markdown from a Format Run

To keep Markdown out of Ruff entirely, exclude it in `pyproject.toml`:

```toml
[tool.ruff]
extend-exclude = ["*.md"]
```

To scope the exclusion to the formatter and leave the rest of the configuration alone:

```toml
[tool.ruff.format]
exclude = ["*.md"]
```

Both are ignored for a path passed explicitly on the command line, so `ruff format README.md` rewrites the file even when it is excluded. Pass `--force-exclude` to close that gap. The `ruff-format` pre-commit hook already sets the flag.

## Skip a Single Code Block

To keep formatting on and protect one block, wrap the fence in HTML comments:

````markdown
<!-- fmt: off -->
```python
ROTATION = [1,  0,  0,
            0,  1,  0]
```
<!-- fmt: on -->
````

An `off` comment with no matching `on` covers the rest of the file. Ruff accepts `<!-- blacken-docs:off -->` and `<!-- blacken-docs:on -->` as equivalents, so a project moving off blacken-docs can leave those comments in place.

The `# fmt: off` and `# fmt: skip` pragma comments also work inside a code block, but they stay visible in the rendered sample. Reach for them when the suppression belongs to one statement. See [how to disable the Ruff formatter for a block of code](https://pydevtools.com/handbook/how-to/how-to-disable-the-ruff-formatter-for-a-block-of-code.md).

## Format Markdown in pre-commit

The upstream `ruff-format` hook ships `types_or: [python, pyi, jupyter]`, so pre-commit never hands it a Markdown file. Override `types_or` to add `markdown`:

```yaml {filename=".pre-commit-config.yaml"}
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.0
    hooks:
      - id: ruff-format
        types_or: [python, pyi, jupyter, markdown]
```

Without the override the hook passes and Markdown drifts out of format.

## Format .qmd and Other Extensions

Ruff discovers `.md` by default. Map any other extension to the Markdown parser with the `extension` setting:

```toml
[tool.ruff]
extension = { "qmd" = "markdown", "markdown" = "markdown" }
```

`ruff format .` then reformats `report.qmd` alongside everything else. The value must be `markdown`; passing `md` fails with `unknown variant`.

## Learn More

- [Ruff formatter documentation](https://docs.astral.sh/ruff/formatter/)
- [Ruff `extension` setting](https://docs.astral.sh/ruff/settings/#extension)
- [Ruff v0.16.0 release announcement](https://astral.sh/blog/ruff-v0.16.0)
- [Ruff 0.16.0 Enables 7x More Rules by Default](https://pydevtools.com/blog/ruff-0-16-0-default-rules.md) covers the rest of the 0.16 upgrade.
- [How to migrate from Black to the Ruff formatter](https://pydevtools.com/handbook/how-to/how-to-migrate-from-black-to-ruff-formatter.md)
