Customization
There are three ways to work with an expander registry.
Bundled entries come from checked-in LanguageBundle data with source IDs and
review notes. Custom entries are instance-local application policy and do not
need to follow the bundled source ledger. A custom context expansion may use
DATE, but ambiguous spellings should still receive an explicit guard;
bundled date detection remains bounded and provider-neutral.
Isolated registries
Expander(...) is a small mutable facade around a new, isolated registry:
from abbr2words import Expander
expander = Expander("en")
expander.add("Tech.", "Technology")
assert expander("Tech. works") == "Technology works"
get_expander(...) also returns a new isolated implementation registry. Changes
to either form do not affect the shared convenience API.
Entries can be case-sensitive and can use regular-expression guards:
expander.add("in.", "inch", only_if_preceded_by=r"\d\s*$")
expander.add("KI", "Künstliche Intelligenz", case_sensitive=True)
Entries use fixed expansion text by default. For reviewed lexical phrases that should compose with sentence position, opt into sentence casing:
expander.add("Pág.", "página", case_policy="sentence")
assert expander("Pág. 12") == "Página 12"
assert expander("consulte la Pág. 12") == "consulte la página 12"
The sentence policy uppercases the first cased character only at input or
after sentence-ending punctuation, including an opening quote or bracket that
follows that boundary. A colon is not a sentence boundary. Dotted abbreviation
matches preserve one final period when their consumed dot is sentence-final;
commas, semicolons, and internal dots are not added or moved.
Ambiguous English dotted forms
The English registry prefers reversible letter readings when a dotted spelling
could be either a semantic abbreviation or a person’s initials. The structural
fallback recognizes standalone uppercase forms with two through eight dotted
letters, such as E.D. and F.C.S.C.J., after registered abbreviations and
reviewed units have had priority. Thus lowercase e.g. expands to for example,
while uppercase E.G. is rendered as E G; I.D. is rendered as I D.
Single-letter N., S., E., and W. use letter defaults and require bounded
address/street evidence before expanding to a direction. This protects personal
initials and biological names such as S. aureus. D.C. expands to District of Columbia only with explicit Washington/place evidence; L.A. remains L A
in this layer so author initials are not rewritten as a city name.
Followed-by guards are evaluated against the suffix immediately after the
candidate abbreviation. In only_if_followed_by=r"^\s*\d", ^ therefore
means “immediately after this abbreviation,” even when the candidate occurs
after other source text. Preceded-by guards continue to use their bounded
prefix window.
Registration is validated immediately. Abbreviations and expansions must be non-empty strings; context keys and values, POS labels, booleans, and guard patterns have stable type/value checks. Guard regexes are compiled once and are trusted application configuration; do not pass arbitrary untrusted regex text because the standard-library engine has no portable timeout.
Boundaries use (?<!\w) and (?!\w). Exact case-sensitive custom entries
outrank bundled exact entries and case-insensitive fallbacks, so conflicts do
not depend on registration order. A preceding guard must end immediately before
the abbreviation after horizontal whitespace is removed.
POS guards are optional and are evaluated only when source-aligned annotations are supplied:
from abbr2words import Expander, TokenAnnotation
expander = Expander("en")
expander.add("Ref.", "Reference", only_if_pos="NOUN")
assert expander.expand("Ref.", annotations=[TokenAnnotation(0, 4, "NOUN")]) == "Reference"
Pass a collection when several labels are accepted. A deny constraint wins if both sets match:
expander.add("Code.", "Code", only_if_pos={"NOUN", "PROPN"}, not_if_pos="PROPN")
Structural guards and reviewed numeric unit matching run before POS guards. POS output is treated as an optional signal; missing labels do not veto an entry, and a general tagger cannot disable a valid numeric unit expression.
Abbreviation expansion remains lexical. Number/date/decimal/identifier parsing,
article contraction, surrounding grammar, and TTS-oriented rendering belong to
the downstream normalizer, such as spokenform.
Unit customization
Units are a separate reviewed inventory, not ordinary abbreviation entries. Use the instance-local unit methods when an application needs an override:
expander.set_unit("kg", "custom kilogram")
expander.remove_unit("kg")
Calling add("kg", ...) or abbreviation removal for a known unit raises a
clear error rather than silently changing a registry that expansion ignores.
Unit overrides do not leak between isolated expanders.
set_unit() retains the reviewed canonical ID when replacing a bundled symbol.
Pass canonical_id="..." to explicitly assign a different identity, or omit it
for a new user-defined symbol. The optional category distinguishes custom
units from currencies or magnitudes. remove_unit() accepts either a symbol or
a canonical ID; removing an ID suppresses all bundled aliases for that identity.
The same rules are available through Expander.iter_unit_matches().
For semantic consumers, prefer the structured matcher over parsing replacement
strings. Numeric quantity matching has priority over lexical abbreviation
matching, so numeric 1 Mio. produces one structured magnitude claim while
standalone Mio. retains its ordinary German abbreviation behavior.
Finite aliases and exact replacements
Bundled entries can have finite aliases for reviewed formatting variants. For
example, the German registry accepts z.B., z. B., z . b ., and zB with
the same boundary policy and replacement metadata. Aliases are registry data,
not global regular-expression substitutions, so attached strings such as
pizzaB, ModellzB12, and du.a.test remain unchanged.
Use the replacement result when a caller needs semantic provenance instead of reconstructing edits with a text diff:
result = expander.expand_with_replacements("Prof. Klein, S. 12")
for replacement in result.replacements:
print(replacement.source, replacement.start, replacement.end)