Skip to main content

indgroup

Format and strictly parse numbers with Indian lakh and crore grouping (12,34,567) and the digit grouping of every Unicode CLDR locale: 1.234.567,89 in German, 1'234'567.89 in Swiss German, a narrow no-break space in French, 1234 but 12.345 in Spanish. Exact for int, float and Decimal, typed, zero dependencies.

Install

pip install indgroup

Usage

The Indian system groups the last three digits, then the rest in pairs, so a million reads as 10,00,000 and a crore as 1,00,00,000.

from decimal import Decimal
from indgroup import format_indian, parse_indian

print(format_indian(1234567))
print(format_indian(-12596475))
print(format_indian(Decimal("1234567.50")))
print(format_indian(0.1))
print(format_indian(1234567, separator=" "))
print(repr(parse_indian("12,34,567")))
print(repr(parse_indian("-1,234.50")))
# 12,34,567
# -1,25,96,475
# 12,34,567.50
# 0.1
# 12 34 567
# 1234567
# Decimal('-1234.50')

Any locale, with the symbols and grouping CLDR records for it. grouping takes the useGrouping vocabulary of Intl.NumberFormat: auto honours the locale's minimum grouping digits, min2 asks for at least two digits before the first separator, always groups every number longer than a group, off never groups.

from indgroup import format_number

print(format_number(1234567.89, "de"))
print(format_number(1234567.89, "de-CH"))
print(ascii(format_number(1234567.89, "fr")))
print(format_number(1234567, "hi"))
print(format_number(1234, "es"), format_number(12345, "es"))
print(format_number(1234, "es", grouping="always"))
print(format_number(1234, "en", grouping="min2"))
print(ascii(format_number(-5, "sv")))
# 1.234.567,89
# 1'234'567.89
# '1\u202f234\u202f567,89'
# 12,34,567
# 1234 12.345
# 1.234
# 1234
# '\u22125'

Nothing is rounded: a Decimal keeps every digit it has, trailing zeros included, and a float is read through its shortest round-trip spelling, so 0.1 prints 0.1 and not the binary value it stands for, and a whole float such as 5.0 prints 5, as in Intl. Values with more than a million integer digits are refused with ValueError.

Parsing is strict. It takes the locale's own minus, decimal and group symbols, ASCII digits only, and groups exactly where the locale writes them, or none at all. A value without a fraction comes back as int, one with a fraction as a Decimal with every digit kept, so formatting the result gives the same text again; the one exception is -0, which reads back as the int 0. Surrounding spaces, tabs and line breaks are ignored, but a group symbol is never taken for one, even when it is a no-break space.

from indgroup import number_symbols, parse_number, resolve_locale

print(repr(parse_number("1.234.567,5", "de")))
print(repr(parse_number("12,34,567", "en-IN")))
for text in ["1,234,567", "1 234"]:
    try:
        parse_number(text, "en-IN")
    except ValueError:
        print("rejected", text)
print(number_symbols("en-IN"))
print(resolve_locale("pt_BR.UTF-8"), resolve_locale("zh-TW"), resolve_locale("iw"))
# Decimal('1234567.5')
# 1234567
# rejected 1,234,567
# rejected 1 234
# NumberSymbols(locale='en-IN', decimal='.', group=',', minus='-', primary=3, secondary=2, min_grouping=1)
# pt zh-Hant he

Command line

$ indgroup 1234567 -1234.5
12,34,567
-1,234.5
$ indgroup -l de 1234567.89
1.234.567,89
$ indgroup --parse 12,34,567.50
1234567.50

Values come from the arguments or from stdin, one per line. -l/--locale picks the locale (default en-IN), -g/--grouping the strategy, -p/--parse reads grouped values back, --locales lists the locales. Put -- before a negative value other than plain digits, such as -1e5 or -1.234,5. Errors exit with status 2.

API

Name What it does
format_indian(value, *, separator=",") Indian lakh and crore grouping, any single separator
parse_indian(text, *, separator=",") strict inverse of format_indian, int or Decimal
format_number(value, locale, *, grouping="auto") the locale's symbols and grouping, no rounding
parse_number(text, locale) strict inverse of format_number
number_symbols(locale) the NumberSymbols a locale uses
NumberSymbols frozen value: locale, decimal, group, minus, primary, secondary, min_grouping
resolve_locale(locale) the CLDR locale a tag reads, or LookupError
supported_locales() the 766 CLDR locales, sorted
cldr_version() the CLDR release of the table, 48.0.0
Number int | float | Decimal
Grouping Literal["auto", "min2", "always", "off"]

Wrong types raise TypeError; NaN, infinities, unknown grouping values and malformed text raise ValueError; an unknown language raises LookupError instead of silently reading English.

How it compares with Intl.NumberFormat

The 650 locales ICU ships, the four grouping strategies and a set of integers and decimals are checked against Intl.NumberFormat of Node 24.18 (ICU 78.3, CLDR 48) with Latin digits. They agree everywhere except Swiss French, where the ICU data writes an apostrophe while CLDR 48 gives fr-CH no group symbol of its own, so it inherits the narrow no-break space of fr. This library follows CLDR. Other differences are by design:

  • Digits are always Latin (0 to 9); the native digits some locales default to, such as Arabic-Indic, are out of scope.
  • Values are never rounded; Intl.NumberFormat shows three fraction digits by default.
  • Only the decimal pattern is used: no percent, currency, compact or scientific notation.

Why

Python's locale module depends on what the operating system has installed, the en_IN locale is absent on most systems, and format(n, ",") knows only groups of three. Babel covers CLDR but is a large dependency that rounds to the pattern's fraction digits, and the small packages that mention lakh and crore convert numbers to words or are tied to template filters. This is a plain formatter and a strict parser with exact Decimal support.

Development

pip install -r requirements-dev.txt
python scripts/fetch_upstream.py   # optional: the CLDR, ICU and Intl reference files
pwsh scripts/check.ps1

The table in src/indgroup/_cldr.json is derived by scripts/build_table.py from the CLDR JSON packages. The reference files are not part of the repository; their tests skip while they are absent. INDGROUP_E2E=1 adds a test that builds, installs and drives the wheel.

License

MIT, see LICENSE. The number data is derived from the Unicode CLDR and ships under the Unicode License v3, see LICENSE-UNICODE.

Metadata

Release files for indgroup 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for indgroup 0.2.0
File Size Uploaded Provenance Actions
indgroup-0.2.0.tar.gz 35.5 kB Without provenance Details for indgroup-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for indgroup 0.2.0
File Interpreter ABI Platform Actions
indgroup-0.2.0-py3-none-any.whl Python 3 none any Details for indgroup-0.2.0-py3-none-any.whl

Total release size: 58.5 kB

Release files / indgroup-0.2.0.tar.gz

Download URL indgroup-0.2.0.tar.gz
Size 35.5 kB
Tags Source
SHA-256 checksum
How to use checksums
22f62a904a9050c89ef32cf040b85c21dafeec438ee72de36b09d018cff73754
BLAKE2b-256 checksum
How to use checksums
9cf9895a3e8d5571a635263c4777453d9aae209ea9cfedbe6a759b3686204666
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / indgroup-0.2.0-py3-none-any.whl

Download URL indgroup-0.2.0-py3-none-any.whl
Size 23.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
938b728a3a39773955e70d062b7d97c13f685ba9a33365a32c79c617646d4276
BLAKE2b-256 checksum
How to use checksums
d956831d0120876b7aa6ff431d7711f98199457e4b50c3aba3c96ad08e91efdb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Security for indgroup 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page