WeasyMerge is a tool for doing mail merges and other bulk document generation without having to write your own glue code. You get the power of WeasyPrint's best-in-class PDF generation with the ease of writing a Jinja2 template with some HTML/CSS and providing a CSV file of data to merge.
WeasyMerge can generate individual PDFs or batch rows together into one or more PDFs.
Installation
WeasyMerge is a command-line tool, so the easiest way to install it is with pipx or uv:
pipx install weasymerge
uv tool install weasymerge
You can also install it into an existing environment with pip:
pip install weasymerge
WeasyMerge relies on WeasyPrint, which needs some system libraries — see its installation guide. On Windows this is much smoother under WSL.
Usage
weasymerge --data guests.csv --template invite.html.j2 --output "pdfs/Invite {row_number} {row[Name]}.pdf"
pipx run and uvx run WeasyMerge on demand, fetching it if it isn't already
available, so you can invoke it without installing it first:
pipx run weasymerge --data guests.csv --template invite.html.j2 --output "pdfs/Invite {row_number} {row[Name]}.pdf"
uvx weasymerge --data guests.csv --template invite.html.j2 --output "pdfs/Invite {row_number} {row[Name]}.pdf"
The first row of the CSV file is assumed to be the header row and the header is used as the key for the row dictionary.
HTML escaping is on by default in the Jinja2 template.
The row number for the first data row is 1.
WeasyMerge provides "safe" row data to the output filename template (ASCII letters, digits, space, underscore, hyphen, and dot). Non-ASCII letters are converted to their closest ASCII equivalent.
Batch mode (multiple rows per document)
Pass --rows-per-document=N to render N rows into a single document,
or --rows-per-document=all to render every row into one document.
Use this for label sheets, ID cards, or any layout where the
template needs to see more than one row at a time.
weasymerge --data seeds.csv --template labels.html.j2 \
--output "labels-{batch.from_row_number}-{batch.to_row_number}.pdf" \
--rows-per-document=all
In batch mode the template receives a batch object instead of
row and row_number:
batch.rows— list of row dicts in this batchbatch.numbered_rows— list of(row_number, row)pairsbatch.from_row_number,batch.to_row_number— 1-indexed inclusive rangebatch.total_rows— total rows in the source data
The same fields are available in the --output template (e.g.
{batch.from_row_number}, {batch.rows[0][Plant]}). Page breaks
within a single document are the template's responsibility — use
CSS page-break-after: always or break-after: page.
See examples/labels/ for a 70×37mm, 24-per-A4 label sheet.
Barcodes and QR codes
WeasyMerge comes with barcode and QR code Jinja2 filters, powered by pyStrich.
Each filter turns a value into a data: URL you can drop straight into an
<img> tag in your template:
<img src="{{ row['TicketID'] | qrcode_svg_dataurl }}" alt="Ticket QR code">
The available filters are:
qrcode_svg_dataurldatamatrix_svg_dataurlaztec_svg_dataurlpdf417_svg_dataurlcode128_svg_dataurlean13_svg_dataurlcode39_svg_dataurl
See examples/tickets/ for A6 event tickets with a QR code and a Code128
barcode.
Metadata
Release files for weasymerge 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| weasymerge-0.1.0.tar.gz | 57.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| weasymerge-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.9 kB
Release files / weasymerge-0.1.0.tar.gz
| Download URL | weasymerge-0.1.0.tar.gz |
|---|---|
| Size | 57.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
13dbdbf4952503d658f15e5d812c93064880404019470371badaf4955a17f3e9
|
|
BLAKE2b-256 checksum How to use checksums |
f6ceabe6af7e9dbd8bb713ce41a4ea65e1aaf9d91a9421b73cfa54851e1f50ea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Jul 1, 2026.
Transparency logRelease files / weasymerge-0.1.0-py3-none-any.whl
| Download URL | weasymerge-0.1.0-py3-none-any.whl |
|---|---|
| Size | 6.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
af1328c47499fc29cd48ae951429b9cc25f3af097ec31ca5d0c16fb728a3ff29
|
|
BLAKE2b-256 checksum How to use checksums |
fd73ea6450f93211c39bb5184d7598a2bfe6c0d851eaef6938225c65335b76a9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Jul 1, 2026.
Transparency log