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.