FlowFX

Test-driven Python learning

Bugs are nice because I can learn from them. Recently I found a bug that taught me about floats in Python and how not to use them. I found it thanks to an automated test.

The test

In a legal contract I had to print a monetary amount in a format similar to

MXN $11,600.00 (eleven thousand, six hundred pesos 00/100 cents)

My tests check the LaTeX source of the resulting PDF, looking for the correctly formatted string 11,600.00, which represents an amount of 11,000 something plus a VAT of 16%, and also for the string eleven thousand, six hundred.

It essentially goes like this:

from app.models import LeaseAgreement

def test_contract_states_rent_plus_vat():
    """Test correct statement of rent amount with and without IVA (VAT (USt (MWSt)))."""
        
    contract = LeaseAgreement.build(    
        rent_amount=Decimal(10000.00)
    )

    tex = contract.render_tex()
    
    assert '10,000.00' in tex
    assert '11,600.00' in tex
    assert 'eleven thousand, six hundred' in tex

The first two asserts did not fail, the third one did. My code actually printed

MXN $11,600.00 (eleven thousand, five hundred and ninety-nine pesos 00/100 cents)

The bug

Turns out I had misused Python's Decimal class to calculate the amount plus VAT. Like this:

>>> x = Decimal(10000) * Decimal(1.16)

Printing the number in the desired format works fine.

>>> print('{:,.2f}'.format(x))
11,600.00

But rounding down for only printing the amount without decimal places does not.

>>> num2words(int(x))
'eleven thousand, five hundred and ninety-nine'

That is because floats are not exact.

>>> Decimal(1.16)
Decimal('1.1599999999999999200639422269887290894985198974609375')
>>> int(Decimal(1.16)*10000)
11599

The fix

Working with monetary amounts that round to 2 digits requires care. Thanks to Rami and others, I now know that I should have used e.g.

Decimal('1.16')

and

Decimal('1.16').quantize(Decimal('.01'), rounding=ROUND_HALF_UP)

when apropriate.

Tags: #python # testing Categories: