Skip to main content

immoney

CI Build Status Test coverage report
PyPI Package Python versions

Installation

$ pip install --require-venv immoney

Design goals

These core aspects of this library each eliminate entire classes of bugs:

  • Exposed and internal data types are either immutable or faux immutable.
  • Invalid amounts of money cannot be represented. There is no such thing as 0.001 US dollars, and there is no such thing as negative money.
  • Builtin operations never implicitly lose precision.
  • Built from the ground-up with support for static type checking in mind. This means that bugs that attempt to mix currencies can be found by a static type checker.
  • A comprehensive test suite with 100% coverage, including property tests that assert random sequences of operations behave as expected.

Features

Safe division

In real life we cannot split the subunit of a currency, and so for our abstractions to safely reflect reality, we shouldn't be able to do that in code either. Therefore instead of defining division to return a value with precision loss, the implementation of division for Money returns a tuple of new instances with the value split up as even as possible. This is implemented as Money.__floordiv__.

>>> Money("0.11", SEK) // 3
(Money('0.04', SEK), Money('0.04', SEK), Money('0.03', SEK))

This method of division will always be safe, as it has the guaranteed property that the sum of the instances returned by the operation always equal the original numerator.

Subunit fractions

Sometimes we do need to represent fractions of monetary values that are smaller than the subunit of a currency, for instance as a partial result of a larger equation. For that purpose, this library exposes a SubunitFraction type. This type is used as return type for Money.__truediv__.

>>> SEK(13) / 3
SubunitFraction('1300/3', SEK)

Because there is no guarantee that a SubunitFraction is a whole subunit (by definition ...), converting back to Money can only be done with precision loss.

>>> (SEK(13) / 3).round_money(Round.DOWN)
Money('4.33', SEK)

Overdraft

Again referring to real life, there is no such thing as negative money. Following in the same vein as for not allowing subunits to be split, the value of a Money instance cannot be negative. Instead, to represent for instance a negative balance on an account, this library exposes an Overdraft class that is used as return type of Money.__sub__ when the computed value would have been negative.

>>> balance = SEK(5)
>>> balance - SEK(4)
Money('1.00', SEK)
>>> balance - SEK(5)
Money('0.00', SEK)
>>> balance - SEK("6.50")
Overdraft('1.50', SEK)
>>> balance - SEK("6.50") + SEK("1.50")
Money('0.00', SEK)

Because negative values are encoded as its own type in this way, situations where negative values can result from arithmetic but aren't logically expected, such as for the price of an item in a store, can be discovered with a static type checker.

Type-safe comparison

Instances of Money do not support direct comparison with numeric scalar values. For convenience an exception is made for integer zero, which is always unambiguous.

>>> from immoney.currencies import SEK
>>> SEK(1) == 1
False
>>> SEK(1) >= 1
Traceback (most recent call last):
  ...
TypeError: '>=' not supported between instances of 'Money' and 'int'
>>> SEK(0) == 0
True

Immediate and full instantiation

"2 dollars" is represented exactly the same as "2.00 dollars", in every aspect. This means that normalization of values happen at instantiation time.

Instantiating normalizes precision to the number of subunits of the instantiated currency.

>>> EUR(2)
Money('2.00', EUR)
>>> EUR("2.000")
Money('2.00', EUR)

Trying to instantiate with a value that would result in precision loss raises a runtime error.

>>> EUR("2.001")
Traceback (most recent call last):
  ...
immoney.errors.ParseError: Cannot interpret value as Money of currency EUR ...

Instance cache

Since instances of Money and Currency are immutable it's safe to reuse existing instances instead of instantiating new ones. This happens transparently when instantiating a new Money instance and can lead to faster code and less consumed memory.

Support for localization

Because localization is a large and complex problem to solve, rather than reinventing the wheel, this is mostly outsourced to the Babel library. There's a wrapping function provided around Babel's format_currency, and a dependency "extra" to install a compatible version.

To use immoney.babel, make sure to install a compatible version.

$ pip install --require-venv immoney[babel]

The function can be used with instances of Money and Overdraft.

>>> from immoney.babel import format_monetary
>>> from immoney.currencies import KRW, USD
>>> format_monetary(KRW(1234), locale="KO")
'₩1,234'
>>> format_monetary(USD("12.34"), locale="NB")
'USD\xa012,34'

Because format_monetary is just a simple wrapper, you need to refer to the documentation of Babel's format_currency for the full documentation of accepted parameters and their behavior.

Retrieving currencies by code

Currencies can be retrieved by their codes via immoney.currencies.registry.

>>> from immoney.currencies import registry
>>> registry["NOK"]
Currency(code=NOK, subunit=100)
>>> registry["MVP"]
Currency(code=MVP, subunit=1)
>>> registry["foo"]
Traceback (most recent call last):
  ...
KeyError: 'foo'

Custom currency registries

The library ships with a sensible set of default currencies, however, you might want to use a custom registry for two reasons:

  • You want to use non-default currencies.
  • You only want to allow a subset of the default currencies.

To achieve this, you can construct a custom set of types. It's recommended to use a custom abstract base class for this, this way things will also play nice with the Pydantic integration.

import abc
from typing import Final
from immoney.registry import CurrencyCollector
from immoney.currencies import Currency

__currencies = CurrencyCollector()


class SpaceCurrency(Currency, abc.ABC): ...


class MoonCoinType(SpaceCurrency):
    subunit = 100_000
    code = "MCN"


MCN: Final = MoonCoinType()
__currencies.add(MCN)


class JupiterDollarType(SpaceCurrency):
    subunit = 100
    code = "JCN"


JCN: Final = JupiterDollarType()
__currencies.add(JCN)

custom_registry: Final = __currencies.finalize()

Pydantic V2 support

Install a compatible Pydantic version by supplying the [pydantic] extra.

$ pip install --require-venv immoney[pydantic]

The Currency, Money, SubunitFraction and Overdraft entities can all be used as Pydantic model fields.

>>> from pydantic import BaseModel
>>> from immoney import Money
>>> from immoney.currencies import USD
>>> class Model(BaseModel, frozen=True):
...     money: Money
...
>>> print(instance.model_dump_json(indent=2))
{
  "money": {
    "subunits": 25000,
    "currency": "USD"
  }
}

Developing

It's a good idea to use virtualenvs for development. I recommend using a combination of pyenv and pyenv-virtualenv for installing Python versions and managing virtualenvs. Using the lowest supported version for development is recommended, as of writing this is Python 3.10.

To install development requirements, run the following with your virtualenv activated.

$ python3 -m pip install .[pydantic,test]

Now, to run the test suite, execute the following.

$ pytest

Static analysis and formatting is configured with goose.

# run all checks
$ python3 -m goose run --select=all
# or just a single hook
$ python3 -m goose run ruff-format --select=all

Release files for immoney 0.11.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 immoney 0.11.0
File Size Uploaded
immoney-0.11.0.tar.gz 26.3 kB Details

Built distribution (wheel)

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

Total release size: 49.3 kB

Release files / immoney-0.11.0.tar.gz

Download URL immoney-0.11.0.tar.gz
Size 26.3 kB
Tags Source
SHA-256 checksum
How to use checksums
20b1e2273e371aa9fd8320ae2beb21e84a8d1e47d4fd86aa962766f4c7f14206
BLAKE2b-256 checksum
How to use checksums
d6240aa24b567b25f1d1c81774304d3902b86c3f22037bcf2c7968c6036181e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/5.1.1 CPython/3.12.7

Release files / immoney-0.11.0-py3-none-any.whl

Download URL immoney-0.11.0-py3-none-any.whl
Size 23.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f3e7acee8b87aab75b2fad54d4a49d86cacc41d4983700aadec2dcc4aa665019
BLAKE2b-256 checksum
How to use checksums
0b2ac55bedc9274b572643697255f2910be909dceb372f972467a82485022b13
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/5.1.1 CPython/3.12.7

Release history Release notifications | RSS feed

This release

0.11.0 This release

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.0

2 release files

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