stockholm
This brings a fully featured Money
class for Python 3 – stockholm.Money
.
Library for formatting and performing arithmetic and comparison operations on monetary amounts. Also with support for currency handling, rates, exchange and serialization + deserialization for when transporting monetary amount data across network layers (built-in data generation and parsing). 💰
A library for monetary amounts
- Combining an amount with a currency to create a monetary amount, as they usually should be read, written and transported together.
- Able to work with a plethora of different source types. Human friendly approach with developer experience in mind.
- Get rid of the gotchas if otherwise using
decimal.Decimal
. Sensible rounding by default. Never lose precision when making arithmetic operations. String output as you would expect. - Generate (and parse) structured data to be used in transport layers such as GraphQL or Protobuf.
- Type hinted, battle tested and supporting several versions of Python.
Full feature set further down, but in its simplest form 👇
>>> Money("9001.42", currency="USD")
<stockholm.Money: "9001.42 USD">
Basic examples
Basically stockholm
is a human friendly and modern Money
class for Python 3. This is a library to be used by backend and frontend API coders of fintech companies, web merchants or subscription services. It's great for calculations of amounts while keeping a great level of precision.
from stockholm import Money, Rate
loan_amount = Money("250380.00", currency="EUR")
interest_rate = Rate(0.073)
interest_per_day = loan_amount * (interest_rate / 365)
Comes with functions to produce output for transport layers as well as having a robust and easy way to import/export values in GraphQL, JSON, Protocol Buffers, etc.
interest_per_day.asdict()
interest_per_day.asdict(keys=("amount", "currency"))
interest_per_day.as_protobuf()
The goal is to provide a flexible and robust package for development with any kind of monetary amounts. No more working with floats or having to deal with having to think about values in subunits for data transport layers or losing hours of sleep because of the default way that Decimal
does rounding.
The monetary amounts can be transformed from (or into) dicts, strings, protobuf messages, json, floats, ints, Python Decimals, even other monetary amounts.
from stockholm import Money, Number
gross_price = Money("319.20 SEK")
vat_rate = Number(0.25)
vat_price = gross_price * vat_rate
net_price = gross_price + vat_price
total_sum = net_price * 5
total_sum / 4
Coding applications, libaries and microservices that consume and publish events that contain monetary amounts shouldn't be any harder than anything else. This package aims to ease that work. You can also use it for just numerical values of course.
Real life use-cases
There are times when you want to receive or publish events with monetary amounts or you need to expose an API endpoint and have a structured way to respond with balances, prices, vat, etc. without risking additional weirdness.
If you're developing a merchant solution, a ticketing service or webshop it can be great to have easy and structured interfaces for calculating orders and building summaries or reports.
We don't want to use float
, but you can do more than just rely on int
🤔
Some may be interfacing with banking infrastructure from the 70s or 80s 😓 and has to process data in insanly old string based formats like the example below and validate sums, currencies, etc.
If any of these sounds familiar, a library for handling monetary amounts could help to structure interfaces you build – specially if you're on microservice architectures where code bases quickly gets a life of their own and teams will likely have different takes on their APIs unless strict guidelines (or utility libraries) are in place.
The basic interfaces
from stockholm import Money
The stockholm.Money
object has full arithmetic support together with int
, float
, Decimal
, other Money
objects as well as string
. The stockholm.Money
object also supports complex string formatting functionality for easy debugging and a clean coding pattern.
from stockholm import Money
Money("99.95 USD")
from stockholm import Currency
Currencies to monetary amounts can be specified using either currencies built with the stockholm.Currency
metaclasses or simply by specifying the currency ticker as a string (for example "SEK"
or "EUR"
) when creating a new Money
object.
Most currencies use two decimals in their default output. Some (like JPY) use fractions per default, and a few ones even has more than two decimals.
from stockholm import Currency, Money
Money(1000, "CNY")
Money(1000, Currency.USD)
Money(1000, Currency.JPY)
Currencies using the stockholm.Currency
metaclasses can hold additional options, such as default number of decimals in string output. Note that the amounts behind the scenes actually uses the same precision and backend as Decimal
values and can as well be interchangable with such values, as such they are way more exact to do calculations with than floating point values.
from stockholm import Number, Rate
The Number
and Rate
classes works in the same way and is similar to the Money
class, with the exception that they cannot hold a currency type and cannot operate with sub units. Examples of when to use them could be to differentiate some values from monetary values, while still getting the benefits from the Money
class.
Arithmetic operations between numbers and monetary Money
values will usually result in a returned Money
object. When instantiating a Money
object the currency value can be overriden from the source amount, which could be useful when exchanging currencies.
from stockholm import Money, Rate
jpy_money = Money(1352953, "JPY")
exchange_rate = Rate("0.08861326")
sek_money = Money(jpy_money * exchange_rate, "SEK")
print(f"I have {jpy_money:,.0m} which equals around {sek_money:,.2m}")
print(f"The exchange rate is {exchange_rate} ({jpy_money:c} -> {sek_money:c})")
Installation with pip
Like you would install any other Python package, use pip
, poetry
, pipenv
or your favourite tool.
$ pip install stockholm
To install with Protocol Buffers support, specify the protobuf
extras.
$ pip install stockholm[protobuf]
Topics in more detail
Arithmetics - fully supported
Full arithmetic support with different types, backed by Decimal
for dealing with rounding errors, while also keeping the monetary amount fully currency aware.
from stockholm import Money
money = Money("4711.50", currency="SEK")
print(money)
output = (money + 100) * 3 + Money(50)
print(output)
print(output / 5)
print(round(output / 3, 4))
print(round(output / 3, 1))
Money("100 SEK") + Money("50 SEK")
Money("100 EUR") * 20 + 5 - 3.5
Money("100 USD") - Money("10")
Money("100") - Money("50") + 20
Money("100") + Money(2, currency="GBP")
Money("100", currency="EUR") + Money("10 EUR") - 50 + "20.51 EUR"
Money("100", currency="SEK") + Money("10 EUR")
Money(1) + Money("55 EUR") + Money(10, currency="EUR").to_currency("USD")
Money("5 EUR") * Money("5 EUR")
Formatting and advanced string formatting
Use f-string formatting for more human readable output and money.as_string()
function to output with additional (or less) zero-padded fraction digits.
from stockholm import Money
amount = Money("13384711 USD")
human_readable_amount = f"{amount:,m}"
amount_without_unnecessary_decimals = amount.as_string(min_decimals=0)
Advanced string formatting functionality.
from stockholm import Money, Rate
jpy_money = Money(1352953, "JPY")
exchange_rate = Rate("0.08861326")
sek_money = Money(jpy_money * exchange_rate, "SEK")
print(f"I have {jpy_money:,.0m} which equals around {sek_money:,.2m}")
print(f"The exchange rate is {exchange_rate} ({jpy_money:c} -> {sek_money:c})")
print(f"{sek_money}")
print(f"{jpy_money:.0f}")
print(f"{sek_money:.2f}")
print(f"{sek_money:.1f}")
print(f"{sek_money:.0f}")
print(f"{sek_money:.2m}")
print(f"{sek_money:.4m}")
print(f"{sek_money:+,.4m}")
print(f"{sek_money:.4M}")
print(f"{sek_money:c}")
Currency class
Use stockholm.Currency
types for proper defaults of minimum number of decimal digits to output in strings, etc. All ISO 4217 currency codes implemented, see https://github.com/kalaspuff/stockholm/blob/master/stockholm/currency.py for the full list.
from stockholm import Currency, Money, get_currency
from stockholm.currency import JPY, SEK, EUR, IQD, USDCoin, Bitcoin
print(Money(4711, SEK))
print(Money(4711, EUR))
print(Money(4711, JPY))
print(Money(4711, IQD))
print(Money(4711, USDCoin))
print(Money(4711, Bitcoin))
print(Money(1338, Currency.JPY))
print(Money(1338, get_currency("JPY")))
Parsing input
Input data types in flexible variants
Flexible ways for assigning values to a monetary amount using many different input data types and methods.
from decimal import Decimal
from stockholm import Money
Money(100, currency="EUR")
Money("1338 USD")
Money("0.5")
amount = Decimal(5000) / 3
Money(amount, currency="XDR")
money = Money("0.30285471")
Money(money, currency="BTC")
cents_as_str = "471100"
money = Money(cents_as_str, currency="USD", from_sub_units=True)
money.sub_units
List arithmetics - summary of monetary amounts in list
Adding several monetary amounts from a list.
from stockholm import Money
amounts = [
Money(1),
Money("1.50"),
Money("1000"),
]
Money.sum(amounts)
sum(amounts)
Use in Pydantic models
Money
objects can be used in Pydantic (Pydantic>=2.2
supported) models and used with Pydantic's JSON serialization and validation – the same goes for Number
and Currency
objects as well. Specify the stockholm.Money
type as the field type and you're good to go.
from pydantic import BaseModel
from stockholm import Money
class Transaction(BaseModel):
reference: str
amount: Money
transaction = Transaction(reference="abc123", amount=Money("100.00", "SEK"))
json_data = transaction.model_dump_json()
Transaction.model_validate_json(json_data)
It's also possible to use the stockholm.types
Pydantic field types, for example stockholm.types.ConvertibleToMoney
, which will automatically coerce input into a Money
object.
from pydantic import BaseModel
from stockholm import Money
from stockholm.types import ConvertibleToMoney
class ExampleModel(BaseModel):
amount: ConvertibleToMoney
example = ExampleModel(amount="4711.50 USD")
example.model_dump_json()
Other similar field types that can be used on Pydantic fields are ConvertibleToNumber
, ConvertibleToMoneyWithRequiredCurrency
and ConvertibleToCurrency
– all imported from stockholm.types
.
Note that it's generally recommended to opt for the more strict types (stockholm.Money
, stockholm.Number
and stockholm.Currency
) when possible and the coercion types should be used with caution and is mainly suited for experimentation and early development.
Conversion for other transport medium (for example Protocol Buffers or JSON)
Easily splittable into units
and nanos
for transport in network medium, for example using the google.type.Money
protobuf definition when using Protocol Buffers.
from stockholm import Money
money = Money("22583.75382", "SEK")
money.units, money.nanos, money.currency_code
Money(units=22583, nanos=753820000, currency="SEK")
Monetary amounts can also be exported to dict
as well as created with dict
value input, which can be great to for example transport a monetary value in JSON.
from stockholm import Money
money = Money("4711.75", "SEK")
dict(money)
money = Money.from_dict({
"value": "4711.75 SEK",
"units": 4711,
"nanos": 750000000,
"currency_code": "SEK"
})
The money.asdict()
function can be called with an optional keys
argument, which can be used to specify a tuple of keys which shuld be used in the returned dict.
The default behaviour of money.asdict()
is equivalent to money.asdict(keys=("value", "units", "nanos", "currency_code"))
.
Values to use in the keys
tuple for stockholm.Money
objects are any combination of the following:
key | description | return type | example |
---|
value | amount + currency code | str | "9001.50 USD" |
units | units of the amount | int | 9001 |
nanos | number of nano units of the amount | int | 500000000 |
currency_code | currency code if available | str | None | "USD" |
currency | currency code if available | str | None | "USD" |
amount | the monetary amount (excl. currency code) | str | "9001.50" |
from stockholm import Money
Money("4711 USD").asdict(keys=("value", "units", "nanos", "currency_code"))
Money("4711 USD").asdict(keys=("amount", "currency"))
Money(nanos=10).asdict(keys=("value", "currency", "units", "nanos"))
Using Protocol Buffers for transporting monetary amounts over the network.
from stockholm import Money
money = Money("4711.75", "SEK")
money.as_protobuf()
money.as_protobuf().SerializeToString()
money = Money.from_protobuf(b'\n\x03SEK\x10\xe7$\x18\x80\xaf\xd0\xe5\x02')
from stockholm import MoneyProtobufMessage
message = MoneyProtobufMessage()
message.units = 2549
message.nanos = 990000000
message.currency_code = "USD"
money = Money.from_protobuf(message, proto_class=MoneyProtobufMessage)
message.SerializeToString()
money = Money.from_protobuf(message.remaining_sum)
money.amount
money.units
money.nanos
money.currency
money + 10
money * 31 - 20 + Money("0.50")
Reading or outputting monetary amounts as JSON
from stockholm import Money
money = Money(5767.50, currency="EUR")
money.as_json()
money.as_json(keys=("amount", "currency_code"))
Money.from_json('{"value": "5767.50 EUR", "units": 5767, "nanos": 500000000}')
Money.from_json('{"amount": "5767.500000000", "currency_code": "EUR"}')
Parameters of the Money object
from stockholm import Currency, Money
money = Money("59112.50", currency=Currency.EUR)
money.amount
money.value
money.units
money.nanos
money.currency_code
money.currency
money.sub_units
money.asdict()
money.as_string()
money.as_int()
money.as_float()
money.is_signed()
money.is_zero()
money.to_integral()
money.amount_as_string(min_decimals=4, max_decimals=7)
money.amount_as_string(min_decimals=0)
money.amount_as_string(max_decimals=0)
money.to_currency(currency="SEK")
money.as_json()
money.as_json(keys=("amount", "currency"))
money.as_protobuf()
money.as_protobuf(proto_class=CustomMoneyProtobufMessage)
A simple, yet powerful way of coding with money.
Acknowledgements
Built with inspiration from https://github.com/carlospalol/money and https://github.com/vimeo/py-money