# always-decimal
A tiny Python package to safely convert floats, strings, and numbers into [`Decimal`](https://docs.python.org/3/library/decimal.html) objects.
It solves common headaches when comparing **PostgreSQL `numeric`** values with Python floats by ensuring **consistent `Decimal` values**.
---
## โจ Features
- ๐ **Exact conversion** from `float` โ `Decimal` (no hidden rounding).
- โ๏ธ **Configurable coercion**: set scale, rounding mode, normalize trailing zeros.
- ๐งฉ Works with `float`, `int`, `str`, or `Decimal`.
- ๐ก๏ธ Raises clear `DecimalCoercionError` on invalid inputs.
- ๐ Compatible with Python **3.10+**.
- ๐ฆ Easy install via **uv** or **pip**.
---
## ๐ฆ Installation
### Using [uv](https://github.com/astral-sh/uv) (recommended)
```bash
uv add always-decimal
````
### Using pip
```bash
pip install always-decimal
```
For local development:
```bash
uv pip install -e ".[dev]"
# or
pip install -e ".[dev]"
```
---
## ๐ Usage
```python
from always_decimal import ensure_decimal, to_decimal_exact
# 1. Exact conversion: no rounding, no scale changes
d1 = to_decimal_exact(0.1)
print(d1)
# Decimal('0.1000000000000000055511151231257827021181583404541015625')
# 2. Safe coercion with fixed scale
price = ensure_decimal(19.995, scale=2)
print(price)
# Decimal('20.00') (ROUND_HALF_EVEN default)
# 3. String input with quantization
val = ensure_decimal("1.2345", scale=3)
print(val)
# Decimal('1.235')
# 4. Normalizing (remove trailing zeros)
norm = ensure_decimal("1.2300", scale=4, normalize=True)
print(norm)
# Decimal('1.23')
```
---
## โ๏ธ API
### `to_decimal_exact(value: float | Decimal) -> Decimal`
Convert a `float` or `Decimal` to a `Decimal` **exactly**:
* `float` โ `Decimal.from_float(value)`
* `Decimal` โ returned as-is
Raises `TypeError` for other types.
### `ensure_decimal(value, *, scale=None, rounding=ROUND_HALF_EVEN, clamp_exp=True, normalize=False) -> Decimal`
Convert and coerce input to `Decimal`.
* **scale**: number of fractional digits to quantize (e.g., 2 โ cents).
* **rounding**: rounding mode (default: bankers rounding).
* **clamp\_exp**: clamp exponents to context limits.
* **normalize**: trim trailing zeros.
---
## ๐งช Running Tests
```bash
make test
```
or directly:
```bash
pytest
```
---
## ๐ License
[MIT](LICENSE)
---
## ๐ Contributing
Issues and PRs are welcome!
Please format with `ruff`, type-check with `mypy`, and run `pytest` before submitting.
Raw data
{
"_id": null,
"home_page": null,
"name": "always-decimal",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": "decimal, numeric, postgres, precision",
"author": "Utkarsh Singh",
"author_email": null,
"download_url": "https://files.pythonhosted.org/packages/dc/a8/1a8a5ecb05cdbb6d13562f7a21a80d7c92ae4fb14cc8892dc3491e5828eb/always_decimal-1.0.1.tar.gz",
"platform": null,
"description": "\n# always-decimal\n\nA tiny Python package to safely convert floats, strings, and numbers into [`Decimal`](https://docs.python.org/3/library/decimal.html) objects. \nIt solves common headaches when comparing **PostgreSQL `numeric`** values with Python floats by ensuring **consistent `Decimal` values**.\n\n---\n\n## \u2728 Features\n\n- \ud83d\udd12 **Exact conversion** from `float` \u2192 `Decimal` (no hidden rounding).\n- \u2696\ufe0f **Configurable coercion**: set scale, rounding mode, normalize trailing zeros.\n- \ud83e\udde9 Works with `float`, `int`, `str`, or `Decimal`.\n- \ud83d\udee1\ufe0f Raises clear `DecimalCoercionError` on invalid inputs.\n- \ud83d\udc0d Compatible with Python **3.10+**.\n- \ud83d\udce6 Easy install via **uv** or **pip**.\n\n---\n\n## \ud83d\udce6 Installation\n\n### Using [uv](https://github.com/astral-sh/uv) (recommended)\n\n```bash\nuv add always-decimal\n````\n\n### Using pip\n\n```bash\npip install always-decimal\n```\n\nFor local development:\n\n```bash\nuv pip install -e \".[dev]\"\n# or\npip install -e \".[dev]\"\n```\n\n---\n\n## \ud83d\ude80 Usage\n\n```python\nfrom always_decimal import ensure_decimal, to_decimal_exact\n\n# 1. Exact conversion: no rounding, no scale changes\nd1 = to_decimal_exact(0.1)\nprint(d1)\n# Decimal('0.1000000000000000055511151231257827021181583404541015625')\n\n# 2. Safe coercion with fixed scale\nprice = ensure_decimal(19.995, scale=2)\nprint(price)\n# Decimal('20.00') (ROUND_HALF_EVEN default)\n\n# 3. String input with quantization\nval = ensure_decimal(\"1.2345\", scale=3)\nprint(val)\n# Decimal('1.235')\n\n# 4. Normalizing (remove trailing zeros)\nnorm = ensure_decimal(\"1.2300\", scale=4, normalize=True)\nprint(norm)\n# Decimal('1.23')\n```\n\n---\n\n## \u2699\ufe0f API\n\n### `to_decimal_exact(value: float | Decimal) -> Decimal`\n\nConvert a `float` or `Decimal` to a `Decimal` **exactly**:\n\n* `float` \u2192 `Decimal.from_float(value)`\n* `Decimal` \u2192 returned as-is\n Raises `TypeError` for other types.\n\n### `ensure_decimal(value, *, scale=None, rounding=ROUND_HALF_EVEN, clamp_exp=True, normalize=False) -> Decimal`\n\nConvert and coerce input to `Decimal`.\n\n* **scale**: number of fractional digits to quantize (e.g., 2 \u2192 cents).\n* **rounding**: rounding mode (default: bankers rounding).\n* **clamp\\_exp**: clamp exponents to context limits.\n* **normalize**: trim trailing zeros.\n\n---\n\n## \ud83e\uddea Running Tests\n\n```bash\nmake test\n```\n\nor directly:\n\n```bash\npytest\n```\n\n---\n\n## \ud83d\udcc4 License\n\n[MIT](LICENSE)\n\n---\n\n## \ud83d\ude4c Contributing\n\nIssues and PRs are welcome!\nPlease format with `ruff`, type-check with `mypy`, and run `pytest` before submitting.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Coerce floats/strings/Decimals to Decimal with predictable scale and rounding.",
"version": "1.0.1",
"project_urls": null,
"split_keywords": [
"decimal",
" numeric",
" postgres",
" precision"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "673665fcb697d0d6898486529fe0409a92f33e1279cfad35a2b392fb5f97a26d",
"md5": "767116f0e7f29797e981cbf7d0be06e9",
"sha256": "bf606e7ba2dba22dbe82381d3a0ded9041328580b9adc3d7ceff22c18202a42a"
},
"downloads": -1,
"filename": "always_decimal-1.0.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "767116f0e7f29797e981cbf7d0be06e9",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 4824,
"upload_time": "2025-08-20T00:38:48",
"upload_time_iso_8601": "2025-08-20T00:38:48.432535Z",
"url": "https://files.pythonhosted.org/packages/67/36/65fcb697d0d6898486529fe0409a92f33e1279cfad35a2b392fb5f97a26d/always_decimal-1.0.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "dca81a8a5ecb05cdbb6d13562f7a21a80d7c92ae4fb14cc8892dc3491e5828eb",
"md5": "e6426629bd892e51511189b6f548f31d",
"sha256": "850eaafc53b02cc2c98359e9fdd6753b18e15b91f4f7cac6b59f76ca88d1cf62"
},
"downloads": -1,
"filename": "always_decimal-1.0.1.tar.gz",
"has_sig": false,
"md5_digest": "e6426629bd892e51511189b6f548f31d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 39340,
"upload_time": "2025-08-20T00:38:49",
"upload_time_iso_8601": "2025-08-20T00:38:49.673987Z",
"url": "https://files.pythonhosted.org/packages/dc/a8/1a8a5ecb05cdbb6d13562f7a21a80d7c92ae4fb14cc8892dc3491e5828eb/always_decimal-1.0.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-20 00:38:49",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "always-decimal"
}