Skip to content

A precise library for handling money in Python, with support for multiple currencies, rounding strategies, and allocation methods. Implements Martin Fowler's Money Pattern with 100% test coverage.

License

Notifications You must be signed in to change notification settings

devAbreu/moneyx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

18 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

moneyx - Precise Money Handling for Python

Test Coverage: 100% License: MIT Python Versions Ask DeepWiki

A lightweight, precise, and professional library for handling money in Python applications. moneyx solves precision, rounding, and representation issues that occur when using floating-point types like float for monetary calculations.

Unlike many Python money libraries, moneyx strictly enforces that all monetary amounts are represented with the correct number of decimal places for their currency. For example, 3.678 USD is automatically rejected as invalid (USD allows only 2 decimal places), as is 5.5 JPY (JPY allows 0 decimal places).

πŸ’° Money Pattern

This library implements the Money Pattern as described by Martin Fowler in his book "Patterns of Enterprise Application Architecture" (2002). As Fowler notes:

"A large proportion of the computers in this world manipulate money, so it's always puzzled me that money isn't actually a first class data type in any mainstream programming language."

The Money Pattern creates a first-class representation of monetary values that handles currency, precision, and rounding issues. Learn more about the pattern at Martin Fowler's Money Pattern.

πŸš€ Installation

pip install moneyx

πŸ” Key Features

  • Absolute precision using decimal.Decimal internally
  • Extensive rounding modes (HALF_UP, HALF_DOWN, BANKERS, HALF_ODD, DOWN, UP, CEILING, FLOOR, HALF_TOWARDS_ZERO, HALF_AWAY_FROM_ZERO)
  • Currency validation based on ISO 4217 standards
  • Complete ISO 4217 support with proper symbols, decimal places, and country information
  • Currency lookup by country or numeric code
  • Currency conversion support
  • Proportional allocation of amounts
  • Tax calculation utilities
  • Formatting with locale support
  • Serialization to/from JSON and dictionaries
  • Type annotations for better IDE support
  • High performance with comprehensive benchmarks
  • Thread-safe operations for concurrent environments
  • 100% test coverage with property-based testing

πŸ“š ISO 4217 Data Sources

This library uses official ISO 4217 currency data from:

πŸ“ Usage Examples

Basic Usage

from moneyx import Money

# Create a monetary amount
price = Money("19.99", "USD")

# Basic arithmetic
tax = price.multiply(0.07)  # 7% tax
total = price.add(tax)

print(f"Price: {price.format()}")  # $19.99
print(f"Tax: {tax.format()}")      # $1.40
print(f"Total: {total.format()}")  # $21.39

Working with Cents

Some applications store and calculate money in the smallest currency unit (cents, pence, etc.). Here's how to work with cents in moneyx:

from decimal import Decimal
from moneyx import Money

# Convert from cents to dollars when creating Money objects
cents_amount = 1299  # $12.99 in cents
price = Money(cents_amount / 100, "USD")
print(price.format())  # $12.99

# For more precision, use Decimal
cents_amount = 1299
price = Money(Decimal(cents_amount) / Decimal("100"), "USD")
print(price.format())  # $12.99

# Converting a Money object to cents
dollars = Money("45.67", "USD")
cents = int(dollars.amount * 100)  # 4567
print(f"Amount in cents: {cents}")

# Working with cents directly for JPY (which has 0 decimal places)
yen_amount = 1000  # Β₯1000 (JPY has 0 decimal places)
jpy = Money(yen_amount, "JPY")
print(jpy.format())  # Β₯1,000

Note that moneyx handles the smallest currency unit internally, so you generally don't need to worry about cents-to-dollars conversion unless your application specifically stores amounts in cents.

Different Currencies

from moneyx import Money

# Create amounts in different currencies
usd = Money("100.00", "USD")
eur = Money("85.00", "EUR")

# Convert between currencies
eur_from_usd = usd.convert_to("EUR", rate=0.85)
print(f"{usd.format()} = {eur_from_usd.format_locale('de_DE')}")  # $100.00 = 85,00 €

Currency Information and Lookup

from moneyx import Money
from moneyx.currency import Currency

# Get information about a currency
usd = Money("100.00", "USD")
print(f"Symbol: {usd.currency.symbol}")  # $
print(f"Name: {usd.currency.name}")  # US Dollar
print(f"Decimals: {usd.currency.decimals}")  # 2
print(f"Countries: {', '.join(usd.currency.countries)}")  # Lists all countries using USD

# Find currencies by country
swiss_currencies = Currency.get_by_country("SWITZERLAND")
for currency in swiss_currencies:
    print(f"{currency.code}: {currency.name}")  # CHF, CHE, CHW

# Find currency by numeric code
eur = Currency.get_by_number("978")
print(f"{eur.code}: {eur.name}")  # EUR: Euro

Rounding Modes

from moneyx import Money, RoundingMode

# Classical rounding (HALF_UP)
standard = Money("2.5", "USD")  # 2.50 -> rounds to 3 when needed

# Banker's rounding (round to even)
bankers = Money("2.5", "USD", rounding=RoundingMode.BANKERS)  # 2.50 -> rounds to 2 when needed
bankers2 = Money("3.5", "USD", rounding=RoundingMode.BANKERS)  # 3.50 -> rounds to 4 when needed

# Additional rounding modes
odd = Money("2.5", "USD", rounding=RoundingMode.HALF_ODD)  # 2.50 -> rounds to 3 when needed
odd2 = Money("3.5", "USD", rounding=RoundingMode.HALF_ODD)  # 3.50 -> rounds to 3 when needed

# Directional rounding
ceiling = Money("2.1", "USD", rounding=RoundingMode.CEILING)  # Always rounds up
floor = Money("2.9", "USD", rounding=RoundingMode.FLOOR)  # Always rounds down
towards_zero = Money("2.5", "USD", rounding=RoundingMode.HALF_TOWARDS_ZERO)  # 2.5 -> 2
away_from_zero = Money("2.5", "USD", rounding=RoundingMode.HALF_AWAY_FROM_ZERO)  # 2.5 -> 3

# Negative numbers
neg = Money("-2.5", "USD", rounding=RoundingMode.HALF_TOWARDS_ZERO)  # -2.5 -> -2
neg2 = Money("-2.5", "USD", rounding=RoundingMode.HALF_AWAY_FROM_ZERO)  # -2.5 -> -3

Allocation and Splitting

from moneyx import Money

# Divide a bill proportionally
bill = Money("100.00", "USD")
alice_part = 2  # Alice pays 2 parts
bob_part = 1    # Bob pays 1 part

shares = bill.allocate([alice_part, bob_part])
print(f"Alice pays: {shares[0].format()}")  # $66.67
print(f"Bob pays: {shares[1].format()}")    # $33.33

# Split a bill evenly
bill = Money("100.00", "USD")
equal_shares = bill.split_evenly(3)
for i, share in enumerate(equal_shares):
    print(f"Person {i+1} pays: {share.format()}")  # $33.33, $33.33, $33.34

Tax Calculations

from moneyx import Money

# Add tax to a price
price = Money("100.00", "USD")
with_tax = price.with_tax(10)  # 10% tax
print(f"Price with tax: {with_tax.format()}")  # $110.00

# Extract tax from a tax-inclusive amount
inclusive = Money("110.00", "USD")
tax_info = inclusive.extract_tax(10)  # 10% tax
print(f"Base price: {tax_info['base'].format()}")  # $100.00
print(f"Tax amount: {tax_info['tax'].format()}")   # $10.00

Internationalization

from moneyx import Money

amount = Money("1234.56", "USD")
print(amount.format_locale("en_US"))  # $1,234.56
print(amount.format_locale("es_ES"))  # 1.234,56 $
print(amount.format_locale("de_DE"))  # 1.234,56 $

Serialization

from moneyx import Money
import json

price = Money("99.99", "USD")

# To/from JSON
json_str = price.to_json()
restored = Money.from_json(json_str)

# To/from dictionary
data = price.to_dict()
restored = Money.from_dict(data)

Bulk Operations

from moneyx import Money
from moneyx.bulk import bulk_add, bulk_multiply, bulk_allocate

# Create multiple money objects
items = [
    Money("10.50", "USD"),
    Money("20.75", "USD"),
    Money("5.99", "USD")
]

# Add all items together
total = bulk_add(items)
print(f"Total: {total}")  # $37.24

# Apply different multipliers to each item
multipliers = [1.1, 1.05, 1.2]
adjusted = bulk_multiply(items, multipliers)
for item in adjusted:
    print(item)  # $11.55, $21.79, $7.19

# Allocate money in bulk by ratio
budget = Money("1000.00", "USD")
allocations = [1, 2, 3, 4]  # Ratio 1:2:3:4
shares = bulk_allocate(budget, allocations)
for share in shares:
    print(share)  # $100.00, $200.00, $300.00, $400.00

πŸ”’ Safe Money Handling

moneyx prevents common money-handling errors:

from moneyx import Money
from moneyx.exceptions import PrecisionError, InvalidCurrencyError

try:
    # This will fail - USD allows max 2 decimals
    Money("100.123", "USD")
except PrecisionError as e:
    print(e)  # The currency USD only allows 2 decimal places. Received: 100.123

try:
    # This will fail - XYZ is not a valid currency
    Money("100.00", "XYZ")
except InvalidCurrencyError as e:
    print(e)  # Unknown currency: XYZ

πŸ“Š Performance Benchmarks

moneyx is designed for high performance. Here are some key benchmarks:

Operation Performance
Money creation ~670K ops/sec
Addition ~660K ops/sec
Subtraction ~665K ops/sec
Multiplication ~450K ops/sec
Allocation ~98K ops/sec
HALF_UP rounding ~1.59M ops/sec
BANKERS rounding ~1.75M ops/sec
HALF_ODD rounding ~1.04M ops/sec
Bulk addition ~448K ops/sec
Bulk multiplication ~47K ops/sec
String formatting ~4.3M ops/sec
Locale formatting ~87K ops/sec

Run benchmarks yourself with:

python -m pytest tests/test_benchmark.py --no-cov --benchmark-columns=Min,Max,Mean,Median,OPS

πŸ‘ Acknowledgments

This library was inspired by py-money by Vimeo, while expanding functionality with additional features like multiple rounding strategies, allocation methods, and tax calculation utilities.

πŸ“– License

MIT License - see LICENSE for details.

About

A precise library for handling money in Python, with support for multiple currencies, rounding strategies, and allocation methods. Implements Martin Fowler's Money Pattern with 100% test coverage.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages