Skip to main content

python-mdma

Render one or more Markdown strings from an .mdma template and a typed inputs object.

This is the Python reference implementation of MDMA. See the language specification and docs for the full grammar, filter reference, and worked examples.

Install

pip install -e .

Usage

from mdma import render

source = open("release-notes.mdma").read()
result = render(source, {
    "project": "Acme SDK",
    "version": "3.0.0",
    "date": "2026-07-01",
    "added": ["WebSocket support"],
    "breaking": True,
    "releases": [{"version": "2.1.0", "date": "2026-06-01", "added": ["Dark mode"]}],
})

result["slug"]             # "Acme SDK-3.0.0"          (str)
result["release-notes"]    # rendered markdown          (str)
result["changelog-entry"]  # one string per release     (list[str], from `<multiple:>`)

A <multiple:> block can also declare <name:> to key each item by a computed name instead of array position:

<changelog-by-version>
<multiple: entry in releases>
<name: entry.version>

### {{ entry.version }} — {{ entry.date }}
result["changelog-by-version"]
# {"2.1.0": "### 2.1.0 — 2026-06-01\n", "2.0.0": "### 2.0.0 — 2026-05-01\n"}

render_template(template, inputs) renders an already-parsed template (from parse_file) — same semantics as render, minus the parse:

from mdma import parse_file, render_template

template = parse_file(source)  # parse once ...
render_template(template, {"project": "Acme SDK", "version": "3.0.0", "date": "2026-07-01"})  # ... render many times

render_file(path, inputs) reads path as UTF-8 and renders it — equivalent to render(open(path).read(), inputs):

from mdma import render_file

result = render_file("release-notes.mdma", {"project": "Acme SDK", "version": "3.0.0", "date": "2026-07-01"})

write_output(result, output_dir, block=None) writes a render()/render_file() result to .md files. Omit block to write every top-level block; pass a block name to write only that one. A string-valued block becomes {output_dir}/{block}.md; a <multiple:> block becomes a directory {output_dir}/{block}/ with one file per item — {name}.md if the block declared <name:>, otherwise {index}.md. Returns the list of Paths written.

from mdma import render_file, write_output

result = render_file("release-notes.mdma", {...})
write_output(result, "out/")                        # every block
write_output(result, "out/", block="release-notes")  # just that one

get_inputs(source) returns the template's @inputs declarations, and validate_inputs(source, inputs) checks an inputs dict against them without rendering — returning the resolved inputs (defaults applied) or raising MissingInputError / MdmaTypeError:

from mdma import get_inputs, validate_inputs

get_inputs(source)
# [InputDecl(name="project", type="string", has_default=False, default=None), ...]

validate_inputs(source, {"project": "Acme SDK", "version": "3.0.0", "date": "2026-07-01"})
# resolved inputs, with declared defaults applied

parse_file(source) is also exported for lower-level access to the parsed template (inputs and blocks).

render() raises one of the exceptions in mdma.errors on failure:

Exception Condition
MissingInputError a required input (no default) was not supplied
MdmaTypeError an input's runtime type doesn't match its declared type, or a <name:> expression evaluates to something other than a string/number
MdmaReferenceError a forward block reference, or an undefined variable
FilterError a filter was applied to a value of the wrong type
MdmaSyntaxError the .mdma source doesn't conform to the grammar (including <name:> used without a preceding <multiple:>)
DuplicateNameError two items in a <multiple:> block computed the same <name:> value

All are subclasses of mdma.errors.MdmaError.

Behavioral notes not obvious from spec.md

  • The blank line conventionally left between one block's content and the next block's header (or EOF) is treated as file formatting, not part of either block's rendered value — it's stripped from both ends of the block body before parsing. This is required for block references ({{ blockname }}) to be safely embeddable inline; otherwise every block value would carry a stray trailing newline from that separator. Blank lines inside a body are preserved exactly as written.
  • Whitespace control ({%-/-%}) is applied per-tag, exactly as written — a conditional branch that renders empty does not retroactively remove surrounding blank-line text unless that text is trimmed by an adjacent -.
  • Accessing a missing property on an object/object[] value (e.g. entry.description when description wasn't set) yields None rather than raising — objects are untyped maps, so this is normal and is what makes | default(...) useful on them. An undefined root identifier (typo'd variable/block/input name) still raises MdmaReferenceError.
  • default([]) and other array literals ([a, b]) are supported in expressions even though the formal grammar doesn't enumerate an array-literal production — the filter reference relies on this syntax ({{ list | default([]) }}).
  • multiple is a reserved word (can't be used as a block or input name), but name is not — <name:> is only ever recognized in its fixed position right after <multiple:>, so an input or block literally named name (e.g. name: string) is unaffected.

Development

pip install -e ".[dev]"
pytest

Metadata

Release files for python-mdma 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for python-mdma 0.4.0
File Size Uploaded
python_mdma-0.4.0.tar.gz 23.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for python-mdma 0.4.0
File Interpreter ABI Platform
python_mdma-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 45.4 kB

Release files / python_mdma-0.4.0.tar.gz

Download URL python_mdma-0.4.0.tar.gz
Size 23.8 kB
Tags Source
SHA-256 checksum
How to use checksums
5d2c2116970bd29331a747885e77ccf1692cfc4b377becf8eaded8120c8506d0
BLAKE2b-256 checksum
How to use checksums
22200e091dab8b5e88ce6231cfb440417cee5f1195e01de6d05fdd0cdb28530e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 14, 2026.

Transparency log

Release files / python_mdma-0.4.0-py3-none-any.whl

Download URL python_mdma-0.4.0-py3-none-any.whl
Size 21.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
20faa2f2536220954aa9a1fe479555ee2ed495daa4005ece83b2f734bc8a125f
BLAKE2b-256 checksum
How to use checksums
442d849af47e18397b1828ca38ea21a3a09a699b4b46a43249e351e873685957
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page