Getting started¶
Installation¶
pefftacular needs Python 3.12 or later and has no runtime dependencies.
A file to work with¶
The examples on this page use this small PEFF file. Save it as proteins.peff:
# PEFF 1.0
# GeneralComment=Two-entry example for the pefftacular docs
# //
# DbName=SwissProt
# Prefix=sp
# DbVersion=2026_03
# DbSource=https://www.uniprot.org
# NumberOfEntries=2
# SequenceType=AA
# //
>sp:P69905 \PName=Hemoglobin subunit alpha \GName=HBA1 \NcbiTaxId=9606 \TaxName=Homo sapiens \Length=30 \SV=2 \PE=1 \ModResPsi=(4|MOD:00046|O-phospho-L-serine)
MVLSPADKTNVKAAWGKVGAHAGEYGAEAL
>sp:P68871 \PName=Hemoglobin subunit beta \GName=HBB \NcbiTaxId=9606 \TaxName=Homo sapiens \Length=30 \SV=2 \PE=1 \VariantSimple=(7|V|sickle cell)
MVHLTPEEKSAVTALWGKVNVDEVGGEALG
Reading¶
Everything at once: read_peff¶
read_peff returns the file header and a list of entries:
from pefftacular import read_peff
header, entries = read_peff("proteins.peff")
print(len(entries))
# 2
for entry in entries:
print(entry.prefix, entry.db_unique_id, entry.gname, entry.pname)
# sp P69905 HBA1 Hemoglobin subunit alpha
# sp P68871 HBB Hemoglobin subunit beta
One entry at a time: PeffReader¶
PeffReader parses the header up front and then yields entries lazily, so memory use stays flat
on large databases. It must be used as a context manager: the file is opened on entering the
with block and closed on leaving it, and using the reader outside one raises RuntimeError:
from pefftacular import PeffReader
with PeffReader("proteins.peff") as reader:
print(reader.header.databases[0].db_name)
for entry in reader:
print(entry.db_unique_id, len(entry.sequence))
# SwissProt
# P69905 30
# P68871 30
Paths, file objects and strings¶
Both readers accept a path (str or pathlib.Path) or any text-mode file object. A str is
always treated as a path. A path may be gzip, bzip2 or xz compressed (proteins.peff.gz);
the format is detected from the file's first bytes, not its name. Pipes and FIFOs work too. To parse PEFF text you already hold in memory, wrap it in
io.StringIO:
import io
from pefftacular import read_peff
text = """# PEFF 1.0
# //
# DbName=Demo
# Prefix=db
# DbVersion=1
# DbSource=local
# NumberOfEntries=1
# SequenceType=AA
# //
>db:X1 \\PName=Demo protein
PEPTIDE
"""
header, entries = read_peff(io.StringIO(text))
print(entries[0].pname, entries[0].sequence)
# Demo protein PEPTIDE
The data model¶
Everything pefftacular returns is a frozen dataclass: immutable and comparable with ==.
SequenceEntry is not hashable, because custom_values and extra are dicts; use
(entry.prefix, entry.db_unique_id) as a key instead.
FileHeader
├── peff_version "1.0"
├── general_comments file-level "# GeneralComment=" lines
└── databases one DatabaseHeader per "# //" block
└── DatabaseHeader
├── prefix, db_name, db_version, db_sources, number_of_entries, ...
├── custom_key_defs CustomKeyDef, one per "# CustomKeyDef=" line
└── optional_tag_defs OptionalTagDef, one per "# OptionalTagDef=" line
SequenceEntry one per ">" description line
├── prefix, db_unique_id, sequence
├── pname, gname, ncbi_tax_id, tax_name, length, sv, ev, pe, ...
├── variant_simple, variant_complex, mod_res_unimod, mod_res_psi, mod_res,
│ processed, disulfide_bond, proteoform (tuples of annotation objects)
├── custom_values keys declared by a CustomKeyDef
└── extra any other \Key=value pair, as raw strings
The file header¶
from pefftacular import read_peff
header, entries = read_peff("proteins.peff")
print(header.peff_version)
# 1.0
print(header.general_comments)
# ('Two-entry example for the pefftacular docs',)
db = header.databases[0]
print(db.prefix, db.db_name, db.db_version, db.number_of_entries)
# sp SwissProt 2026_03 2
print(db.db_sources, db.sequence_type)
# ('https://www.uniprot.org',) AA
A PEFF file can hold several databases, each with its own prefix. Every entry's prefix names
the database it belongs to, so you can match them up:
dbs = {db.prefix: db for db in header.databases}
for entry in entries:
print(entry.db_unique_id, "from", dbs[entry.prefix].db_name)
# P69905 from SwissProt
# P68871 from SwissProt
Header keys pefftacular does not model (for example SpecificKey blocks) are kept as raw
strings in DatabaseHeader.extra.
Sequence entries¶
The description line >sp:P69905 \PName=... \GName=HBA1 ... becomes a SequenceEntry.
The part before the colon is prefix, the part after is db_unique_id, and each standard key
maps to a typed field:
| PEFF key | Field | Type |
|---|---|---|
| (prefix) | prefix |
str |
| (unique id) | db_unique_id |
str |
| (sequence lines) | sequence |
str |
\ID |
id |
str \| None |
\DbUniqueId |
db_unique_id_key |
str \| None |
\PName |
pname |
str \| None |
\GName |
gname |
str \| None |
\NcbiTaxId (or \OX) |
ncbi_tax_id |
int \| None |
\TaxName |
tax_name |
str \| None |
\Length |
length |
int \| None |
\SV / \EV / \PE |
sv / ev / pe |
int \| None |
\Decoy |
decoy |
bool \| None |
\Comment |
comment |
str \| None |
\VariantSimple |
variant_simple |
tuple[VariantSimple, ...] |
\VariantComplex |
variant_complex |
tuple[VariantComplex, ...] |
\ModResUnimod |
mod_res_unimod |
tuple[ModResUnimod, ...] |
\ModResPsi |
mod_res_psi |
tuple[ModResPsi, ...] |
\ModRes |
mod_res |
tuple[ModRes, ...] |
\Processed |
processed |
tuple[Processed, ...] |
\DisulfideBond |
disulfide_bond |
tuple[DisulfideBond, ...] |
\Proteoform |
proteoform |
tuple[Proteoform, ...] |
| custom keys | custom_values |
dict[str, tuple[CustomKeyValue, ...]] |
| anything else | extra |
dict[str, str] |
alpha = entries[0]
print(alpha.ncbi_tax_id, alpha.tax_name, alpha.length, alpha.sv, alpha.pe)
# 9606 Homo sapiens 30 2 1
print(alpha.mod_res_psi[0])
# ModResPsi(positions=(4,), accession='MOD:00046', name='O-phospho-L-serine', tag=None, annot_id=None)
Annotation types are covered in detail in the annotations guide.
Changing an entry¶
Models are frozen, so "editing" means making a copy with dataclasses.replace:
from dataclasses import replace
renamed = replace(alpha, gname="HBA2")
print(alpha.gname, renamed.gname)
# HBA1 HBA2
Writing¶
write_peff(header, entries, dest) writes a complete file. dest is a path or a text-mode file
object; entries can be any iterable, including a generator. It is consumed once, as a stream,
and nothing is written until every entry has been checked.
from pefftacular import DatabaseHeader, FileHeader, SequenceEntry, write_peff
header = FileHeader(
peff_version="1.0",
databases=(
DatabaseHeader(
prefix="my",
db_name="MyProteins",
db_version="1.0",
db_sources=("in-house",),
number_of_entries=1,
sequence_type="AA",
),
),
)
entry = SequenceEntry(
prefix="my",
db_unique_id="PROT001",
sequence="MKTIIALSYIFCLVFA",
pname="Example protein",
gname="EXMP",
length=16,
)
write_peff(header, [entry], "output.peff")
print(open("output.peff").read())
# PEFF 1.0
# //
# DbName=MyProteins
# Prefix=my
# DbVersion=1.0
# DbSource=in-house
# NumberOfEntries=1
# SequenceType=AA
# //
>my:PROT001 \Length=16 \PName=Example protein \GName=EXMP
MKTIIALSYIFCLVFA
The writer:
- emits keys in a fixed canonical order, so output is stable across runs,
- wraps sequences at 60 residues per line,
- backslash-escapes
\,|and unbalanced parentheses inside annotation fields.
Every line the writer produces is parsed back and compared with the entry, so a value that would
read back differently raises PeffWriteError. That check is about three quarters of the write
time. For entries read from a file and not changed, skip it with
write_peff(header, entries, dest, verify=False).
It does not fill in Length or NumberOfEntries for you. Set them yourself if you want
them in the file; the reader warns if they disagree with the data.
Filtering a file¶
Read, transform, write. PeffReader streams the input; write_peff collects the entries it is
given into a list before writing, so the entries you keep must fit in memory:
from pefftacular import PeffReader, write_peff
with PeffReader("proteins.peff") as reader:
header = reader.header
kept = [e for e in reader if e.variant_simple]
write_peff(header, kept, "with_variants.peff")
print([e.db_unique_id for e in kept])
# ['P68871']
If you filter entries out, the header's NumberOfEntries still says the old count and the
reader will warn when it reads the new file. Update it with dataclasses.replace:
from dataclasses import replace
db = replace(header.databases[0], number_of_entries=len(kept))
write_peff(replace(header, databases=(db,)), kept, "with_variants.peff")
Errors and warnings¶
Parse errors¶
Input that cannot be parsed at all raises PeffParseError. It carries line, context (the
offending text) and hint. Every pefftacular exception derives from PeffError, which is a
ValueError.
import io
from pefftacular import PeffError, PeffParseError, read_peff
try:
read_peff(io.StringIO("#PEFF 1.0\n>sp:P1\nMK\n"))
except PeffParseError as err:
print(err.line)
print(err.context)
print(err.hint)
# 1
# #PEFF 1.0
# The first non-blank line must be exactly '# PEFF 1.0' (note the space after '#')
The context and hint are also attached as exception notes, so they appear in a traceback.
write_peff raises PeffWriteError for entries it cannot serialize, such as an empty sequence.
Spec-violation warnings¶
Reading is permissive. If a file breaks a PEFF MUST rule but can still be parsed (a missing
mandatory header key, a \Length that does not match the sequence, an annotation position
outside the sequence, a wrong NumberOfEntries), you get the data plus a PeffWarning:
import io
import warnings
from pefftacular import PeffWarning, read_peff
bad = """# PEFF 1.0
# //
# DbName=Demo
# Prefix=db
# DbVersion=1
# DbSource=local
# NumberOfEntries=1
# SequenceType=AA
# //
>db:X1 \\Length=99
PEPTIDE
"""
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
header, entries = read_peff(io.StringIO(bad))
print(entries[0].sequence)
# PEPTIDE
for w in caught:
print(w.category.__name__, w.message)
# PeffWarning Entry 'X1': Length=99 but sequence has 7 residues
For strict parsing, turn the warnings into errors:
warnings.simplefilter("error", PeffWarning)
try:
read_peff(io.StringIO(bad))
except PeffWarning as w:
print("rejected:", w)
# rejected: Entry 'X1': Length=99 but sequence has 7 residues
PeffWarning subclasses UserWarning, so existing UserWarning filters still apply. The
deprecated \Variant= key also warns with PeffWarning, and its value lands in entry.extra.
Logging¶
pefftacular never configures logging itself. To see what it is doing, enable the
pefftacular logger:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("pefftacular").setLevel(logging.DEBUG)
File opens, header parsing and entry counts log at DEBUG; totals read and written log at
INFO, under the pefftacular.parser and pefftacular.writer loggers.