Skip to content

Read, edit, write🔗

Loading🔗

import sil_lift

lex = sil_lift.load("dictionary.lift")

load() accepts any well-formed LIFT 0.13 document — including schema-invalid real-world files. Anything the model doesn't define (unknown elements/attributes, comments) is carried losslessly as LIFT residue in each node's opaque extra field. Other LIFT versions raise LiftParseError naming the version.

The model🔗

Every LIFT element is a typed dataclass: Entry, Sense, Example, Pronunciation, Variant, Relation, Etymology, Reversal, and so on. Multilingual text is a Multitext, which is a Mapping from language code to Text:

entry = lex.find(id="abat")

str(entry.lexical_unit["seh"])          # "abat"
entry.lexical_unit["en"] = "grove"      # plain strings are coerced
"en" in entry.citation                  # False
list(entry.lexical_unit.keys())         # ["seh", "en"]

keys(), values() and items() are views, one key per language, and len() counts languages rather than forms. Both mutators work on the language rather than on one form: del entry.lexical_unit["en"] removes every English form, and assigning to "en" leaves exactly one.

A schema-valid document has nothing more, but real files sometimes repeat a language. forms holds every form in file order, reads answer with the first, and validate reports duplicate-form-lang until you assign to that language. A form carrying no lang at all cannot be created through the mapping, is reported as form-missing-lang, and is dropped when its node re-serializes.

Text is structured — an ordered list of str and Span fragments — because <text> can contain nested <span> markup. str(text) flattens to plain text; the fragments keep the markup for round-tripping.

Glosses are form-shaped in LIFT (each <gloss> carries its own language), so a sense has glosses: list[Form] plus helpers:

sense = entry.senses[0]                 # top level only
sense.gloss("en")                       # Text | None
entry.all_senses()                      # every sense and subsense, document order
entry.gloss_langs()                     # {"en", "id"}, subsenses included

Reach for all_senses() whenever a question concerns the whole entry: counting senses, collecting languages, finding media. entry.senses gives the top level, which is what you want only when the nesting itself matters.

Saving🔗

lex.save()                # back to where it was loaded from
lex.save("elsewhere.lift")

Entries you didn't modify are written back byte-identical; a document you didn't modify at all is byte-identical from the first byte to the last. See Fidelity guarantees for the precise contract.

The entries you did modify go out with a fresh dateModified (and a dateCreated if they had none), so an edit doesn't ship under the date it was loaded with — the tools that merge LIFT decide what changed from that attribute. lex.save(stamp=False) writes the dates the model holds and nothing more; lex.save(when=...) pins the moment instead of reading the clock. See Generated timestamps for the rest of the rules.

Building from scratch🔗

lex = sil_lift.Lexicon(producer="my-script 1.0")
entry = sil_lift.Entry(id="hello", guid="...")
entry.lexical_unit["en"] = "hello"
sense = sil_lift.Sense()
sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"])))
entry.senses.append(sense)
lex.entries.append(entry)
lex.save("new.lift")

Canonical sorting🔗

lex.sort()      # entries by (guid, id); ranges/field defs by id/tag
lex.save()      # untouched entries keep their exact bytes, in the new order

sil_lift.canonicalize("in.lift", "out.lift")   # fully re-serialized, diff-ready

See also: Worked example: bulk-editing glosses.