Adding Decimals to classes with OOP + rounding to significant digits (ATM machine)

I’m learning OOP by trying to build a rudimentary banking ATM machine. It started out an an exercise for Fred Baptiste’s Udemy online courseware but I am now extending and building on top of the feature set just for fun.

Now I am endeavoring to add these features:

  • Using Decimals instead of Floats
  • “Bankers Rounding” (“half-way” amounts should be rounded up) to 2 significant digits for all transactions

This is what I am trying to accomplish today. This might sound weird but based on my testing, I’m not sure if I have succeeded. That’s why I need your help, Pythonistas!

The tutorial I am working with is titled “Python Decimal”. The tutorial demonstrates how to use decimals and rounding. Here is some sample coding running in my trusty Python REPL:

$ bpython
bpython version 0.22.1 on top of Python 3.10.4 /usr/bin/python
>>> import decimal
>>> from decimal import Decimal
>>> ctx = decimal.getcontext()
>>> ctx.rounding = decimal.ROUND_HALF_UP
>>> x = Decimal('2.3456')
>>> x
Decimal('2.3456')
>>> round(x,2)
Decimal('2.35')
>>>

So that works. Here is my script (reduced test case):

import decimal
from decimal import Decimal
from random import randint
 
class Account:
 
   def __init__(self, first_name, last_name,starting_balance=0.00):
       self.first_name = first_name
       self.last_name = last_name
       self.balance = round(Decimal(starting_balance),2)
       self.transaction_id = randint(101,999)
 
   def deposit(self, amount):
       self.balance += round(Decimal(amount),2)
       self.transaction_id += randint(101,999) - randint(101,999)
       return f'D-{self.transaction_id}'
  
   def withdraw(self, amount):         
       self.balance -= round(Decimal(amount),2)
       self.transaction_id += randint(101,999) - randint(101,999)
       return f'W-{self.transaction_id}'

Take note that in my script above I do not get a context or set the rounding to ROUND_HALF_UP. Here is me importing the script in my REPL and instantiating:

>>> import script
>>> BA = script.Account('Winston','Smith')
>>> BA.balance
Decimal('0.00')
>>> BA.deposit(0.505)
'D-547'
>>> BA.balance
Decimal('0.51')
>>> 

Here my script is rounding half up even without specifying .getcontext(). So my script is rounding up when I am expecting it to round down (Python’s default).

Here are my questions for all of you:

First of all, why is my script working when it shouldn’t be?

Secondly, for these two lines:

ctx = decimal.getcontext()
ctx.rounding = decimal.ROUND_HALF_UP
  • …the ctx variable is an instantiation of the .getcontext() function or class method located somewhere inside the decimal package as described in the official docs. Where in this understanding am I correct or incorrect?
  • In the next line, ctx.rounding is declared based on the decimal package’s setting to ROUND_HALF_UP (among a few the other options as covered in the official Python docs). Is this correct? [/list]

My third (and perhaps my most important) question is: How and why does the above ctx instantiation have any bearing on the subsequent lines in the REPL (if any)?:

>>> x = Decimal('2.3456')
>>> x
Decimal('2.3456')
>>> round(x,2)
Decimal('2.35')
>>>

My final question is, where in my OOP script should I place / invoke the .getcontext() method from the decimal package? Inside the scope of the Account class or outside? Or would it be better suited beneath a dunder main declaration at the bottom of the script?

I guess overall here I am just struggling to grasp how, when, or where .getcontext() is used or if it is even necessary at all. Could you Pythonistas kindly clarify?

In most of your tests you hand in a string. But for the .505 one, you hand in a float. That float is not exactly .505 but slightly bigger. So the rounding goes up in all contexts.

The problem with round is explained in an official document.
https://docs.python.org/3/library/functions.html?highlight=round#round

As @BowlOfRed pointed out, you should use a string or a tuple to construct a Decimal.

>>> import decimal
>>> from decimal import Decimal
>>> 
>>> ctx=decimal.getcontext()
>>> ctx.rounding
'ROUND_HALF_EVEN'
>>> Decimal(0.505)
Decimal('0.50500000000000000444089209850062616169452667236328125')
>>> round(Decimal(0.505), 2)
Decimal('0.51')
>>> round(Decimal('0.505'), 2)
Decimal('0.50')

If ndigits of round is greater than 1, it may not function as even rounding. In that case, numpy is one of the options that always returns an evenly rounded value.

>>> import numpy as np
>>> round(0.505, 2)
0.51
>>> np.round(0.505, 2)
0.5