Money
Canonicalizes one money amount paired with a currency per call to CODE amount padded to ISO 4217 minor units.
In plain language: give it
"USD500"or"1.000,50 EUR"and it hands back"USD 500.00"/"EUR 1000.50"if the specs say the amount and currency go together. Currency without an amount — or an amount without a currency — is not Money’s domain (see Currency).
What it recognizes — and what it does not
Section titled “What it recognizes — and what it does not”| Recognizes (currency + amount adjacent) | Does not recognize |
|---|---|
USD500, USD 500.00, EUR 1.000,50 | Currency identifier alone (USD) — use Currency |
€500, CN¥1000, US$ 500 (qualified symbols) | Bare shared symbol without disambiguation ($500 or ¥1000 with default contract — ¥ is shared by CNY/JPY) — recognized but INVALID |
1.000,50 EUR — European comma-decimal: last separator is the decimal point | Amount-glued tokens without a clean boundary — MISSING |
$500 with dollar_sign_currency="USD" — opt-in for shared symbols | Bare $ without dollar_sign_currency — INVALID |
Shared symbol handling follows Currency’s
default_currencyidea but for amounts: Money’sdollar_sign_currencyresolves only to one of the symbol’s own CLDR candidates ($500+USD→USD 500.00, while$500+MYRstaysINVALID—MYRis not a$candidate), exactly like Currency’sdefault_currency.
Canonical output
Section titled “Canonical output”Default output_format is "code_amount" (space between code and amount).
output_format | Renders | Example for USD 500 |
|---|---|---|
(default) code_amount / None / "default" | CODE + space + amount | USD 500.00 |
compact | removes the single space | USD500.00 |
The amount is padded/normalized to the currency’s ISO 4217 minor units (e.g. 2 decimals for USD/EUR). How over-precision is handled depends on precision (see contract).
from paxman.capabilities import Moneyimport paxman
paxman.register_all_shipped()# Money.create_contract() is a contract — use paxman.canonicalize() to get a resultpaxman.canonicalize( "USD500", Money.create_contract()).canonicalized_value # "USD 500.00"paxman.canonicalize( "USD500", Money.create_contract(output_format="compact")).canonicalized_value # "USD500.00"paxman.canonicalize( "1.000,50 EUR", Money.create_contract()).canonicalized_value # "EUR 1000.50"Contract
Section titled “Contract”contract = Money.create_contract( dollar_sign_currency=None, # str | None — uppercase alpha-3, e.g. "USD" precision="strict", # "strict" (default) | "truncate" | "round" — over-precision policy output_format=None, # "code_amount" (default) or "compact" # plus every common field: excluded_rules / pinned_rules / year / extra_grammars)dollar_sign_currency: whenNone(default), a multi-candidate bare symbol amount like$500is recognized but never resolved →INVALID. When set to one of the symbol’s own CLDR candidates, bare$/¥/£resolve to that code (e.g.$+USD→USD 500.00;$+MYRstaysINVALIDsinceMYRis not a$candidate — same guard as Currency’sdefault_currency).€500never needs this —€is definitive forEURand ignoresdollar_sign_currency; qualified symbols likeUS$/CA$/RMare also definitive. An unknown code likeZZZstaysINVALIDvia the minor-unit guard.precision: how to treat more fractional digits than the currency’s minor units allow —strictrejects →INVALID,truncatedrops excess digits,roundhalf-to-even.output_formatnever affects validation — only the space.
paxman.canonicalize("$500", Money.create_contract()).status.value # "invalid"paxman.canonicalize( "$500", Money.create_contract(dollar_sign_currency="USD")).canonicalized_value # "USD 500.00"# Bare "$" resolves only to one of its own CLDR candidates (parity with# Currency's default_currency; non-candidate MYR stays INVALID)paxman.canonicalize( "$500", Money.create_contract(dollar_sign_currency="MYR")).status.value # "invalid"# Unknown code never resolves — INVALID via MINOR_UNITS guardpaxman.canonicalize( "$500", Money.create_contract(dollar_sign_currency="ZZZ")).status.value # "invalid"paxman.canonicalize( "USD 1.999", Money.create_contract(precision="strict")).status.value # "invalid" — too many decimals for USDpaxman.canonicalize( "USD 1.999", Money.create_contract(precision="round")).canonicalized_value # "USD 2.00"Statuses
Section titled “Statuses”| Input | Contract | Status | Why |
|---|---|---|---|
USD500 | defaults | SUCCESS | → USD 500.00 |
€500 | defaults | SUCCESS | → EUR 500.00 (definitive symbol) |
$500 | defaults | INVALID | shared symbol, no dollar_sign_currency |
$500 | dollar_sign_currency="USD" | SUCCESS | → USD 500.00 (USD is a $ candidate) |
$500 | dollar_sign_currency="MYR" | INVALID | MYR is not a $ candidate |
1.000,50 EUR | defaults | SUCCESS | European comma-decimal → EUR 1000.50 |
USD 1.999 | precision="strict" | INVALID | over-precision → rejected |
hello | any | MISSING | no Money pattern |
| Two different amounts | any | raises MultipleMentionsError | split first |
Notebook snippet — clean a column with mixed formats
Section titled “Notebook snippet — clean a column with mixed formats”import paxmanfrom paxman.capabilities import Moneyfrom paxman.core.domain import Resolutionfrom paxman.core.errors import CapabilityError, ContractError, MultipleMentionsError
paxman.register_all_shipped()c_strict = Money.create_contract()c_usd = Money.create_contract(dollar_sign_currency="USD")c_round = Money.create_contract(precision="round")
rows = ["USD500", "€500", "$500", "1.000,50 EUR", "USD 1.999", "hello"]
for text in rows: for label, c in [("strict", c_strict), ("USD bare-$", c_usd), ("round", c_round)]: try: r = paxman.canonicalize(text, c) except (MultipleMentionsError, CapabilityError, ContractError) as e: print(f"{text!r:15} [{label:10}] → exception {type(e).__name__}: {e}") continue val = r.canonicalized_value if r.status == Resolution.SUCCESS else "—" print(f"{text!r:15} [{label:10}] → {r.status.value:10} {val!r}")Provenance
Section titled “Provenance”- ISO 4217:2015 — currency codes and minor units (List One, as amended through #180, snapshot 2026-01-01 via SIX; provenance
PUBLICATIONyear 2015,https://www.iso.org/iso-4217-currency-codes.html).CURRENCY_CODESholds 165 codes with numeric minor units (13 N.A. codes excluded);MINOR_UNITSmaps exponent (0 for JPY/KRW, 2 for most, 3 for BHD, 4 for CLF/UYW). - Unicode CLDR v47 (2025-03-13) — currency symbols and English display names (
https://cldr.unicode.org/,https://cldr.unicode.org/downloads/cldr-47). Word recognition is case-insensitive (any casing ofEuro/euro/EUROresolves toEUR); symbols are case-exact (leivsLei). Newer CLDR v48/48.1 (2025-10 and 2026-01) exists — regeneration planned viatools/regenerate_currency_data.py. - Amount parsing: last separator wins, single separator always decimal (
1,00→1,1.234→1.234); grouping with multiple separators folds base-1000 (1,00.50→1000.50); narrow NBSP (U+202F) is the only space-grouping form — ASCII1 234.56is not grouped (see Limitations).
Limitations
Section titled “Limitations”- Single separator is always decimal — no thousand grouping.
USD 1,000→SUCCESS USD 1.00, notUSD 1000.00. A lone,/.is read as the decimal point per the lockedparse_amounttable, so thousand-grouped input without a decimal part mis-canonicalizes. Treating a single separator with exactly 3 trailing digits as grouping would need a spec ruling (see follow-up); until then, pre-normalize thousand-grouped amounts before calling. - ASCII space never groups.
USD 1 234.56does not parse as grouped thousands; only narrow NBSP (U+202F) groups with spaces.
Compare Currency for identifier-only canonicalization (no amount).
See also: Execution Result, Segmentation.