citerra
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
Documentcontains entries, comments, preambles, string definitions, source-order blocks, diagnostics, and validation helpers.Entryexposes the citation key, entry type, fields, source text, semantic helpers, and field mutation methods.Fieldexposes the original field name, parsed value, optional raw source text, and optional source location.Valuerepresents string literals, numbers, variables, and concatenations.Diagnosticreports 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)
| File | Size | Uploaded | |
|---|---|---|---|
| citerra-0.4.0.tar.gz | 348.3 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| 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
|