Custom units and explicit registration
Define a multiplicative unit from an existing Unit, exact scale or positive
finite scalar Quantity. .define() creates a named unit with its own formal
identity. Only an explicit definition block registers spellings immediately.
UnitCatalog collects public unit attributes after its class body finishes.
Absolute coordinate definitions follow the same rule; see the
AffineUnit API.
>>> from unitx import Quantity, Registry, Unit, UnitCatalog
>>> from unitx.si import si
>>> newton = Unit('kg*m/s2').define('demoN', 'demo newton')
>>> minute = (60 * si.s).define('demoMin', 'demo minute', prefixable=False)
>>> Quantity(2, minute).value_in(si.s)
120
>>> Quantity(1, si.kN).value_in(newton)
1000
These units are immediately usable as objects. Independent definitions may have identical symbols without colliding. Two such Unit objects remain formally distinct, while physically compatible quantities can interact.
Model and responsibilities
The main objects separate unit meaning, spelling lookup and persistence:
flowchart TB
D["UnitDefinition<br/>named definition and physical metadata"]
P["PrefixDefinition<br/>prefix definition"]
A["SingleUnit<br/>prefix + unit definition"]
U["Unit<br/>extra scale + formal product"]
Q["Quantity[T]<br/>magnitude + linear unit"]
C["AffineUnit<br/>linear origin + difference unit"]
AQ["AffineQuantity[T]<br/>linear coordinate reading"]
LC["LogUnit<br/>reference + logarithmic scale + convention"]
LQ["LogQuantity[T]<br/>logarithmic reading"]
R["Registry + UnitParser<br/>spelling environment and parsing"]
K["UnitCatalog<br/>read-only attribute interface"]
M["Factor / Dimension / Monomial<br/>exact values and formal algebra"]
D --> A
P --> A
A --> U
U --> Q
U --> C
C --> AQ
U --> LC
LC --> LQ
M --> U
R -->|binds text to definitions| U
K -->|provides attributes and a parser| R
UnitDefinition owns the unit's semantic facts. Registry decides which
spellings resolve in one environment; it does not change a definition's meaning.
UnitCatalog groups public unit objects and their parser. Reconstruction uses
stable locations to find existing definitions and checks their saved meaning;
it does not create or register replacement definitions.
UnitDefinition is valid as soon as construction succeeds: it checks its own
spellings, prefix permissions and conversion metadata without a Registry.
Import this low-level record and PrefixPolicy from unitx.unitdefinition.
Registry owns all spelling indexes and checks only conflicts with its current
bindings. Prefix specifications are shared immutable data, independent of
definitions and parsing environments.
Coordinates retain a complete unit product for both their physical reference and their difference scale, including any extra exact factor. Catalogue construction first collects its explicit units and public attributes, then binds them in one call, and finally publishes reconstruction locations. Definitions already in a shared Registry are not implicitly published by an unrelated catalogue.
Independent registries
Most users can use built-in catalogues directly. A Registry is an explicit spelling environment for private definitions or a file format:
Defining a unit and binding its spellings are separate steps. .define() checks
the definition's own spellings and returns a usable Unit object. A Registry then
checks those spellings against its existing bindings before adding them; a
conflict leaves that Registry unchanged.
>>> private = Registry([si.m, si.s])
>>> with private.defining():
... span = (2 * si.m).define('span', 'survey span', 'survey distance')
... speed = (span / si.s).define('spanrate', 'survey speed')
>>> private.parse('[survey span] / s') == span / si.s
True
>>> private.parse('kilosurvey distance') == private.parse('kspan')
True
>>> Quantity(3, span).value_in(si.m)
6
>>> other = Registry()
>>> another_span = other.define(2 * si.m, 'span', 'survey span', 'survey distance')
>>> span == another_span
False
>>> 1 * span == 1 * another_span
True
Registry() starts with only the multiplicative identity. Its constructor and
include(*units) share existing definitions, preserving identity, aliases and
prefix permissions. Named AffineUnit objects also bring their bound reference
and difference definitions. Including an already included definition is harmless.
An entire include() call validates before updating the receiving indexes.
For example, private.include(si.m, si.s) includes both units in one batch.
Registry.define(source, symbol, name, ...) is a convenience for one linear
Unit or positive real Quantity: it creates and binds using that explicit target,
even inside another definition block or catalogue's class body. To bind an existing definition
without creating a new identity, use registry.include(unit).
Select shared definitions before adding private spellings that conflict with
them. For example, an environment can bind B to byte by omitting SI bel and
calling registry.define(Unit(), 'B', 'byte'). There is no fallback from a private
registry to the default table, and parsing never guesses from a source dimension.
Explicit definition blocks
Use with registry.defining(): to define several related units in ordinary
Python code. No class declaration is required. as optionally returns the
same Registry, which remains usable after the block exits:
>>> from unitx import AffineUnit
>>> with Registry([si.K]).defining() as thermal:
... step = (2 * si.K).define('demoStep', 'demo temperature step')
... reading = AffineUnit(10 * si.K, delta_unit=step).define('demoReading', 'demo reading')
>>> thermal.parse('demoStep/K') == step / si.K
True
>>> (3 @ reading).value_in(si.K)
Fraction(16, 1)
Unit, Quantity, AffineUnit and LogUnit .define() calls, including helper calls, use the
selected environment. Existing objects are included with registry.include();
assigning a local variable does not create a Registry attribute.
Nested definition blocks restore the outer target on exit, including exceptions.
The innermost explicit block takes precedence, including while a class body
is executing. An explicit other_registry.define(...) always writes to its named target.
Successfully registered definitions remain after an exception; the block selects
an environment rather than providing a transaction. An individual failed
registration leaves spelling indexes unchanged.
Each new definition in an ordinary block has its own identity. To share one between registries, include the existing unit object. Definitions do not accept user-assigned identity strings.
Only definition construction consults the scope. Bare Unit('m'),
Quantity(3, 'm') and .to('cm') always use the default environment. A private
unit expression must be parsed with registry.parse() or catalogue.parse();
when defining one unit from another, use its bound Unit object directly.
Explicit block selection follows ContextVar semantics: asyncio tasks inherit context at creation and can outlive the parent's block. Changes in one task do not select a registry for another task. Context isolation does not synchronize access to a shared Registry; the concurrent-use contract below still applies.
Concurrent use and read-only access
A fully initialized Registry supports concurrent parse() calls while its
bindings remain unchanged. Finish catalogue imports and registration before
sharing the registry with worker threads. Each call creates its own UnitParser;
callers using UnitParser directly must use separate instances or synchronize
access to a shared parser's mutable parsing state.
>>> from concurrent.futures import ThreadPoolExecutor
>>> shared = Registry([si.m, si.s])
>>> with ThreadPoolExecutor(max_workers=2) as workers:
... parsed = list(workers.map(shared.parse, ('m/s', 'km/s')))
>>> parsed == [si.m / si.s, si.km / si.s]
True
Registry provides no internal locking. If registration can overlap other uses,
the application must synchronize every writer and every reader of that registry,
covering each complete registration or parse call with the same lock. This
includes include(), define(), definitions inside defining() and catalogue
collection into a shared registry. A failed registration leaves the bindings
unchanged, but this guarantee does not make a successful update atomically
visible to concurrent readers. A defining() block neither holds a lock nor
commits all its definitions as one transaction.
There is no public freeze or read-only snapshot API. Read-only use is an
application discipline. UnitCatalog prevents rebinding its attributes; it does
not freeze its associated Registry. With registry=shared, later registrations
in shared are also visible through the catalogue's parse() method.
Registry([unit, ...]) creates independent bindings for explicitly supplied
definitions, retaining their identities, aliases and prefix policies; it does
not copy an entire registry or disable future registration. Already constructed
Unit objects retain their definitions when either registry is extended.
Catalogue class bodies
Catalogue maintainers can use class example(UnitCatalog): to collect a read-only
namespace and parser in one level of indentation. The body runs as ordinary
Python code; after it finishes, public Unit and named AffineUnit/LogUnit attributes are
included together. Helper results are collected when assigned to public attributes;
unexported temporary definitions are not collected. Private attributes are ignored.
New spellings are unavailable during the body. Build later units from earlier
objects, such as (2 * span).define('doubleSpan'). Bare strings continue to use
the default parser. An explicit with registry.defining(): still applies to every
call within its ordinary block extent, including calls inside a class body.
Collection does not override that block or undo its completed registrations.
A failed class body does not reach collection. A collection conflict leaves the receiving Registry unchanged. Neither case installs an implicit definition scope. See defining a catalogue for declaration and identity rules.
Explicitly extending the default environment
Advanced applications can opt into shared default spellings:
>>> from unitx.unitregistry import default_registry
>>> interval = default_registry.define(7 * si.s, 'demoInterval', 'demo interval', prefixable=False)
>>> Unit('demoInterval') == interval
True
Alternatively create a standalone unit and call default_registry.include(unit).
The default environment starts with the agreed built-in SI catalogue; ordinary
.define() calls and extra catalogue imports do not extend it implicitly.
Exact extra scales
Multiplying or dividing a Unit by a Factor or SymbolicConstant creates a
Unit. Its scale is relative to the formal unit product; its factor
includes both. A scale of one compares equal to the corresponding Unit and
has the same hash. Unit multiplication, division and powers involving a
Unit retain that type even when the scale cancels to one.
>>> from unitx.factor import pi
>>> degree = ((pi / 180) * Unit()).define('demoDeg', 'demo degree', prefixable=False)
>>> degree.factor == pi / 180
True
Calling .define() returns an ordinary Unit with its own identity, sharing
the total dimension and SI factor. Copying and pickling preserve the formal
units and extra scale; see reconstruction.
Definition signature and spellings
The symbol is positional-only. The optional primary name accepts positional or keyword
input and defaults to the symbol. Additional name aliases are optional positional arguments after name.
Pass the primary name positionally when supplying aliases. symbol_aliases
accepts a string or tuple of strings; it and prefixable are keyword-only.
The primary name is optional and defaults to the symbol, so .define('twip')
suffices when both spellings are the same.
Symbols and symbol aliases must be single printable unit tokens. Names and name
aliases may contain tokens separated by single ASCII spaces or hyphens, without
leading or trailing separators. Decimal digits and parser operators are invalid.
The primary symbol and any explicitly supplied name must be nonempty; empty aliases are ignored. Square
brackets and superscript characters ⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻ are reserved for parser syntax.
A complete registered multiword name is recognized before parsing a product.
Within expressions, use [survey span] / s or the unit's symbol. A definition
may not give its own aliases conflicting prefix meanings. When binding to a
registry, its texts must also avoid every existing meaning, including generated
prefixes and implicit products such as metre second.
Quantity definitions require positive finite int, Fraction, Decimal or float
magnitudes. Fraction and Decimal scales stay exact. Float input follows Factor's
denominator limit and can warn or round to zero and fail, for example at 1e-10.
Defining a unit never evaluates symbolic scales numerically.
Prefix permissions
Omitting prefixable uses the defining registry's default, or True outside a
block. Registry(prefixable=False) disables prefixes for definitions constructed
inside its defining() block or through its define() method. Catalogue collection
preserves the policy already stored in each definition. Included SI units retain
their original permissions. An explicit per-definition
policy overrides the default. prefixable accepts True for all spellings,
False for none, one spelling string, or a tuple of exact symbols, names and aliases.
A string is equivalent to a singleton tuple. An empty tuple disables prefixes.
Unknown spellings and non-string entries are errors. This policy belongs to the
immutable definition, whether or not it has been bound to a parser.
>>> ticks = Registry([si.s])
>>> tick = ticks.define(
... si.s,
... 'demoTick',
... 'survey interval',
... symbol_aliases='demoT',
... prefixable=('demoT', 'survey interval'),
... )
>>> ticks.parse('[millisurvey interval]') == ticks.parse('mdemoT')
True
>>> Quantity(1000, ticks.parse('mdemoT')).value_in(tick)
Fraction(1, 1)
>>> ticks.parse('mdemoT').expression()
'mdemoT'
Permissions are checked on the spelling before alias normalization. Preferred
output uses the first permitted symbol; if only a name permits the prefix,
expression() emits a [name] reference. Prefix names attach to the first word,
for example kilosurvey interval; a separated kilo survey interval is invalid.
SingleUnit.name falls back to a symbol when no name permits that prefix.
PrefixPolicy is available from unitx.unitdefinition for annotations.
Defining a catalogue
A catalogue combines a read-only namespace with its own parser:
>>> from typing import Final
>>> class laboratory(UnitCatalog, units=(si.m,)):
... span: Final = (2 * si.m).define('span', 'laboratory span')
... double_span: Final = (2 * span).define('doubleSpan', 'double span')
>>> laboratory.parse('span/m') == laboratory.span / si.m
True
>>> (1 * laboratory.double_span).value_in(si.m)
4
UnitCatalog supplies namespace immutability, collection, and parse().
Its class body exposes unit objects as attributes. Use Final
annotations on published attributes for static rebinding diagnostics.
units=shares existing definitions alongside public unit attributes, after the body finishes.metre = si.malso includes the existing definition; Python attribute names do not become parser aliases. Usesymbol_aliases=when needed.- Each class starts with a fresh environment. Inherited attributes are available
normally, but parser bindings are not inherited. Use
registry=when a catalogue deliberately shares an existing Registry; SI uses the default one. - Prefix permissions are fixed during
.define(). Imperial and US catalogue maintainers use explicitRegistry(prefixable=False).defining()blocks with the catalogue's receiving registry. The collector does not change metadata or infer policies from class names. - Definitions use shared immutable objects as formal identities. Including an existing unit preserves its identity. Module-level catalogue declarations supply structured persistence locations with separate module, catalogue and symbol fields. Moving or renaming a published catalogue changes its location. No public identity namespace or per-definition ID is needed.
Independent definitions remain distinct each time a declaration executes,
including when reloading the same module-level catalogue. They do not become the same unit merely because
their class names or symbols match. Identity and hashing are final before
.define() returns, including inside a class body.
Reconstruction and exchange
expression() describes a formal product in an environment that includes its
definitions. It does not choose a receiving parser or include a Unit's extra
scale. Default-resolvable units keep their familiar repr. Standalone and private
units use an internal identity-based repr requiring their definitions to be loaded.
Repr is a Python diagnostic format, not a portable interchange protocol.
Coordinate and point reprs use a spelling shorthand only when the default
environment resolves it to the same coordinate. A private coordinate may share
a spelling with a default linear or logarithmic unit; its repr then uses the
loaded definition's identity rather than reinterpreting that spelling.
Pickle first resolves the live UUID handle, then a structured catalogue location when loading in another process. It checks saved immutable metadata recursively, including coordinate references and difference definitions. It never creates definitions, merges independently constructed objects, or binds spellings. Import the same module-level catalogue declarations before unpickling in another process. Definitions without a published module-level catalogue location, including units kept only in ordinary blocks or local classes, have process-local locators and cannot be recreated by repeating the same spelling. Pickles from older unitx formats are unsupported; load them with the matching older release and recreate the values with the current API.
JSON format 2 also checks the explicit receiving environment. See serialization for receiver examples. Unknown or mismatched definitions fail instead of being registered automatically. Neither format exports a self-contained set of custom definitions. The application supplies shared definition objects within a process, or compatible module-level catalogue declarations in each process.
Documentation index · Units and quantities · SI spelling conventions
Low-level definitions
UnitDefinition is the immutable registry record. Its ordinary constructor is
UnitDefinition(symbol, name=None, *name_aliases, factor=one,
dimension=DIMENSIONLESS, symbol_aliases=(), prefixable=True).
The object itself supplies formal identity; its spelling and physical scale do
not determine equality. Copying preserves that object. Registry indexes and
coordinate views derive from its stored data.
Use Unit.define() or Quantity.define() to derive conversion metadata from
an existing unit, and AffineUnit.define() or LogUnit.define() for a coordinate. Their internal
constructors preserve logarithmic degree and bound coordinate data automatically.
The ordinary constructor does not expose logarithmic, log_degree, reference,
delta, kind, or persistent identity options.