Skip to main content

BibTeX parser for Python

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

citerra-0.4.0.tar.gz (348.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

citerra-0.4.0-cp38-abi3-win_amd64.whl (496.5 kB view details)

Uploaded CPython 3.8+Windows x86-64

citerra-0.4.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (571.7 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ x86-64

citerra-0.4.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (491.3 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ ARM64

citerra-0.4.0-cp38-abi3-macosx_11_0_arm64.whl (523.5 kB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

citerra-0.4.0-cp38-abi3-macosx_10_12_x86_64.whl (556.1 kB view details)

Uploaded CPython 3.8+macOS 10.12+ x86-64

File details

Details for the file citerra-0.4.0.tar.gz.

File metadata

  • Download URL: citerra-0.4.0.tar.gz
  • Upload date:
  • Size: 348.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for citerra-0.4.0.tar.gz
Algorithm Hash digest
SHA256 cb354eab488fb791bda5df298ccdf825c4e97e940bd0bce8c301abc9ad8c2dad
MD5 da099972bdf1a08088802d296c25cc74
BLAKE2b-256 aa9da4c5ffcfc4a223066a9c63262d00c71a31b6af633574589648f378b34b48

See more details on using hashes here.

File details

Details for the file citerra-0.4.0-cp38-abi3-win_amd64.whl.

File metadata

  • Download URL: citerra-0.4.0-cp38-abi3-win_amd64.whl
  • Upload date:
  • Size: 496.5 kB
  • Tags: CPython 3.8+, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for citerra-0.4.0-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 c9f85f07dcadda7434919976673d4e0f7f922926d65b27b444c9f59b63ce9fc4
MD5 258affe59f38a6f79a3545560a2f8353
BLAKE2b-256 3c4c99f84c1a677cb3f7b61e266aa708ba0d13d80fac94f98c0feb16b3f8ad6a

See more details on using hashes here.

File details

Details for the file citerra-0.4.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for citerra-0.4.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 00d9c889b5bc6b15d7de48fabda285d2f31c340297e63cea3e21a6b16340203e
MD5 83fca30c6583bcf8c28e0de00acce109
BLAKE2b-256 6286d2c679ccba4ee32d66dcdbf8658cbfdd78ab1364bad4e591b155c7be37b2

See more details on using hashes here.

File details

Details for the file citerra-0.4.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for citerra-0.4.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 a61e40a45bae8722fc618d7c4773c1c8c761b8a9cd22341e4588c46d68f4c48d
MD5 65b9ffcfeb6ed778834ea09974f9ab4f
BLAKE2b-256 c1a9d688c497bd966ff7abd9c5b8d12cc8f9c250caf97efbfcef91c8bb4bca54

See more details on using hashes here.

File details

Details for the file citerra-0.4.0-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for citerra-0.4.0-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c8ffe0a8672ec281a7cb1ebf6629f05ac23b64786256d016577e921dc4293db8
MD5 1d7f7be476c6486ba37966c7778f2ab8
BLAKE2b-256 e52e37b4548b274f9a6fb06c554eb7e5c4877af786d3c3cf9b7c3872206c0f86

See more details on using hashes here.

File details

Details for the file citerra-0.4.0-cp38-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for citerra-0.4.0-cp38-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 53f8c7ebb4d0f03fd3b7aa01a4ab9adb9169fcd5315679805e9e0bdc6a9b60e0
MD5 2242d437b161db5fd9d8fe6d919b179b
BLAKE2b-256 8dcf82039fc9d98cce205d38be473a6b25277b688567b5beb9f64756a5068785

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page