Skip to main content

citerra

CI PyPI Python License

BibTeX parser for Python.

citerra parses, validates, edits, and writes BibTeX documents. It supports strict parsing by default, opt-in tolerant recovery, diagnostics with source locations, raw-text retention, source-preserving writes, name/date/identifier helpers, and plain-record projection for application code.

The package is distributed as ABI3 wheels for Python 3.8 and newer.

Performance Snapshot

Measured on tests/fixtures/tugboat.bib: 2,701,551 bytes, 73,993 lines, and 3,644 entries. Hardware was AMD Ryzen 5 5600G, 6 cores / 12 threads. Measured on 2026-05-14 with Python 3.11.14; throughput is input-size normalized.

For a BibTeX parser, the relevant speed measurements are:

Workload Why it matters citerra result
Structured parse Load a bibliography into entries and fields for application logic 0.008 s, 322.2 MiB/s
Source-preserving parse Keep raw text, source locations, diagnostics, and source-order blocks for tools 0.011 s, 232.8 MiB/s
Raw-preserving write Write retained source text after low-churn edits 0.002 s, 1149.8 MiB/s
Normalized write Serialize structured data with configured formatting 0.010 s, 259.7 MiB/s

The comparison used citerra 0.3.0, bibtexparser 1.4.4, bibtexparser 2.0.0b9, and pybtex 0.26.1. citerra structured parse disables source capture and raw preservation for the closest parser-output comparison. Relative time is normalized to the first row in each table.

Python parser / mode Version Output retained Median parse time Throughput Approx. entries/s Relative time
citerra structured parse 0.3.0 Entries, fields, strings, comments, preambles 0.008 s 322.2 MiB/s 455.7k 1.0x
citerra source-preserving parse 0.3.0 Structured data, raw text, locations, diagnostics 0.011 s 232.8 MiB/s 329.3k 1.4x
bibtexparser parse 2.0.0b9 Entries/library model 0.367 s 7.0 MiB/s 9.9k 45.9x
pybtex parse 0.26.1 Bibliography data 0.863 s 3.0 MiB/s 4.2k 107.9x
bibtexparser parse 1.4.4 Entries/database model 10.758 s 0.2 MiB/s 0.34k 1345.2x
Python writer / mode Version Median write time Throughput Relative time
citerra raw-preserving write 0.3.0 0.002 s 1149.8 MiB/s 1.0x
citerra normalized write 0.3.0 0.010 s 259.7 MiB/s 4.4x
bibtexparser write 1.4.4 0.106 s 24.3 MiB/s 47.4x
bibtexparser write 2.0.0b9 0.497 s 5.2 MiB/s 222.0x
pybtex write 0.26.1 3.790 s 0.7 MiB/s 1691.3x

The workflow table below sums the median parse and write measurements for each parser/version. It is a round-trip estimate for parse-edit-write workloads, not a separate end-to-end benchmark.

Workflow Median time Throughput Relative time
citerra source-preserving parse + raw-preserving write 0.013 s 193.6 MiB/s 1.0x
citerra structured parse + normalized write 0.018 s 143.8 MiB/s 1.3x
bibtexparser 2.0.0b9 parse + write 0.864 s 3.0 MiB/s 65.0x
pybtex 0.26.1 parse + write 4.653 s 0.6 MiB/s 349.7x
bibtexparser 1.4.4 parse + write 10.864 s 0.2 MiB/s 816.5x

Reproduction commands are listed in Reproducing Benchmarks.

Install

pip install citerra

The distribution name and import name are both citerra:

import citerra

Parse

import citerra

document = citerra.parse(
    '@article{paper, author = "Jane Doe", title = "Example Paper", year = 2026}',
    expand_values=True,
)

entry = document.entry("paper")
assert entry is not None
assert entry.entry_type == "article"
assert entry.get("title") == "Example Paper"
assert entry.date_parts().year == 2026

File helpers are available:

from pathlib import Path
import citerra

document = citerra.parse_path("references.bib", tolerant=True)
Path("normalized.bib").write_text(citerra.dumps(document), encoding="utf-8")

File-like helpers are also available:

with open("references.bib", encoding="utf-8") as handle:
    document = citerra.load(handle, tolerant=True)

text = citerra.dumps(document)

Document Model

  • Document contains entries, comments, preambles, string definitions, source-order blocks, diagnostics, and validation helpers.
  • Entry exposes the citation key, entry type, fields, source text, semantic helpers, and field mutation methods.
  • Field exposes the original field name, parsed value, optional raw source text, and optional source location.
  • Value represents string literals, numbers, variables, and concatenations.
  • Diagnostic reports parse or validation problems with stable codes and source locations when available.

Tolerant Parsing And Diagnostics

text = '''
@article{ok, title = "Good"}
@article{bad, title = "Missing close"
@book{recovered, title = "Recovered"}
'''

document = citerra.parse(
    text,
    tolerant=True,
    capture_source=True,
    preserve_raw=True,
    source="refs/main.bib",
)

if document.status != "ok":
    for diagnostic in document.diagnostics:
        span = diagnostic.source
        if span is None:
            print(diagnostic.code, diagnostic.message)
        else:
            print(diagnostic.code, span.line, span.column, diagnostic.message)

Raw Text And Source-Preserving Writes

text = '@article{paper, title = "Example Paper"}'

document = citerra.parse(
    text,
    tolerant=True,
    capture_source=True,
    preserve_raw=True,
)

entry = document.entry("paper")
if entry is not None:
    print(entry.raw)
    print(entry.field("title").raw_value)

Use WriterConfig(preserve_raw=True) for low-churn output that reuses retained source text where possible. Use WriterConfig(preserve_raw=False) for normalized structured output.

document.rename_key("paper", "paper-v2")
document.set_field("paper-v2", "note", "accepted")
document.remove_export_fields(["abstract", "keywords"])

config = citerra.WriterConfig(
    preserve_raw=True,
    trailing_comma=True,
)
output = document.write(config)

Plain Records

Some application code wants ordinary dictionaries for filtering, indexing, or bulk transforms. citerra provides explicit helpers for that shape without changing the document model. Use write_entries() for new normalized output from records:

document = citerra.parse_path("references.bib")
records = citerra.document_to_dicts(document)

selected = [record for record in records if record.get("year") == "2026"]
text = citerra.write_entries(
    selected,
    field_order=["author", "title", "journal", "year", "doi"],
    sort_by=["ID"],
    trailing_comma=True,
)

Plain records use ENTRYTYPE and ID keys for the entry type and citation key. Values may be plain strings, integers, or Value objects.

For low-churn edits to a whole parsed document, overlay records back onto the document and then write the document. This preserves retained comments, preambles, string definitions, source order, and unchanged raw field text where possible:

document = citerra.parse_path("references.bib")
records = document.to_dicts()

for record in records:
    if record.get("doi"):
        record["note"] = "checked"

summary = document.update_from_dicts(records)
text = document.write()

Use value_mode="value" when applications need macro and concatenation-aware records:

records = document.to_dicts(value_mode="value")
records[0]["title"] = citerra.Value.from_bibtex_source('venue # " 2026"')
document.update_from_dicts(records)

Helpers

assert citerra.normalize_doi("https://doi.org/10.1000/XYZ.") == "10.1000/xyz"
assert citerra.latex_to_unicode("Jos\\'e") == "José"

names = citerra.parse_names("Jane Doe and {Research Group}")
assert names[1].literal == "Research Group"

date = citerra.parse_date("2026-05-13")
assert (date.year, date.month, date.day) == (2026, 5, 13)

Reproducing Benchmarks

The comparison script uses whichever optional packages are installed in the active environment. bibtexparser 1.x and 2.x use the same package name, so their rows are measured in separate environments.

python python/benchmarks/compare_parsers.py tests/fixtures/tugboat.bib --iterations 15 --warmups 3
python python/benchmarks/compare_parsers.py tests/fixtures/tugboat.bib --write --iterations 15 --warmups 3

Implementation

citerra is implemented as a native extension. Wheels include the parser engine, so ordinary Python installs do not require a Rust toolchain.

Rust Crate

The Rust crate is published as bibtex-parser on crates.io:

[dependencies]
bibtex-parser = "0.4"

See RUST.md for Rust usage.

Local Build

Use the project manifest for local development:

guix shell -m manifest.scm -- maturin build --release --out target/wheels

For local tests without installing into the user environment, unpack the built wheel into a temporary import directory and run pytest with that directory on PYTHONPATH:

rm -rf target/python-test
python3 - <<'PY'
from pathlib import Path
from zipfile import ZipFile

wheel = sorted(Path("target/wheels").glob("citerra-*.whl"))[-1]
target = Path("target/python-test")
target.mkdir(parents=True, exist_ok=True)
with ZipFile(wheel) as archive:
    archive.extractall(target)
PY
guix shell -m manifest.scm -- env PYTHONPATH=target/python-test python3 -m pytest tests/python

License

Licensed under either of Apache-2.0 or MIT, at your option.

Metadata

Release files for citerra 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 citerra 0.4.0
File Size Uploaded
citerra-0.4.0.tar.gz 348.3 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for citerra 0.4.0
File
citerra-0.4.0-cp38-abi3-win_amd64.whl CPython 3.8 abi3 Windows x86-64 Details
citerra-0.4.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.8 abi3 Linux glibc 2.17+ x86-64 Details
citerra-0.4.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.8 abi3 Linux glibc 2.17+ ARM64 Details
citerra-0.4.0-cp38-abi3-macosx_11_0_arm64.whl CPython 3.8 abi3 macOS 11.0+ ARM64 Details
citerra-0.4.0-cp38-abi3-macosx_10_12_x86_64.whl CPython 3.8 abi3 macOS 10.12+ x86-64 Details

Total release size: 3.0 MB

Release files / citerra-0.4.0.tar.gz

Download URL citerra-0.4.0.tar.gz
Size 348.3 kB
Tags Source
SHA-256 checksum
How to use checksums
cb354eab488fb791bda5df298ccdf825c4e97e940bd0bce8c301abc9ad8c2dad
BLAKE2b-256 checksum
How to use checksums
aa9da4c5ffcfc4a223066a9c63262d00c71a31b6af633574589648f378b34b48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / citerra-0.4.0-cp38-abi3-win_amd64.whl

Download URL citerra-0.4.0-cp38-abi3-win_amd64.whl
Size 496.5 kB
Tags CPython 3.8 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
c9f85f07dcadda7434919976673d4e0f7f922926d65b27b444c9f59b63ce9fc4
BLAKE2b-256 checksum
How to use checksums
3c4c99f84c1a677cb3f7b61e266aa708ba0d13d80fac94f98c0feb16b3f8ad6a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / citerra-0.4.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL citerra-0.4.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 571.7 kB
Tags CPython 3.8 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
00d9c889b5bc6b15d7de48fabda285d2f31c340297e63cea3e21a6b16340203e
BLAKE2b-256 checksum
How to use checksums
6286d2c679ccba4ee32d66dcdbf8658cbfdd78ab1364bad4e591b155c7be37b2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / citerra-0.4.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL citerra-0.4.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 491.3 kB
Tags CPython 3.8 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
a61e40a45bae8722fc618d7c4773c1c8c761b8a9cd22341e4588c46d68f4c48d
BLAKE2b-256 checksum
How to use checksums
c1a9d688c497bd966ff7abd9c5b8d12cc8f9c250caf97efbfcef91c8bb4bca54
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / citerra-0.4.0-cp38-abi3-macosx_11_0_arm64.whl

Download URL citerra-0.4.0-cp38-abi3-macosx_11_0_arm64.whl
Size 523.5 kB
Tags CPython 3.8 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
c8ffe0a8672ec281a7cb1ebf6629f05ac23b64786256d016577e921dc4293db8
BLAKE2b-256 checksum
How to use checksums
e52e37b4548b274f9a6fb06c554eb7e5c4877af786d3c3cf9b7c3872206c0f86
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / citerra-0.4.0-cp38-abi3-macosx_10_12_x86_64.whl

Download URL citerra-0.4.0-cp38-abi3-macosx_10_12_x86_64.whl
Size 556.1 kB
Tags CPython 3.8 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
53f8c7ebb4d0f03fd3b7aa01a4ab9adb9169fcd5315679805e9e0bdc6a9b60e0
BLAKE2b-256 checksum
How to use checksums
8dcf82039fc9d98cce205d38be473a6b25277b688567b5beb9f64756a5068785
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release history Release notifications | RSS feed

This release

0.4.0 This release

6 release files

0.3.1

6 release files

0.3.0

6 release files

0.2.3

6 release files

0.2.2

6 release files

0.2.1

6 release files

0.2.0

6 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