Python Forum
ATM machine (deposits/withdrawals) using OOP
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
ATM machine (deposits/withdrawals) using OOP
#1
I’m trying to write a rudimentary banking ATM machine using Python OOP. The exercise I am working on calls for all kinds of advanced features but I am breaking down the task into smaller pieces so it is easier to test and ask questions and build from there.

Right now I am just trying to initialize a starting balance of $0.00 and make basic deposits and withdrawals. If the user tries to withdraw more than their available balance, Python needs to throw a ValueError indicating “Transaction declined.”

Here is my script and test case so far:
class BankAccount:
   starting_balance = 0.00 # USD
 
   def __init__(self, first_name, last_name):
       self.first_name = first_name
       self.last_name = last_name
 
   def deposit(self, starting_balance, amount):
       balance = starting_balance + amount
       return balance
  
   def withdraw(self, balance, amount):
       try:
           balance = balance - amount
       except balance <= 0.00:
           raise ValueError('Transaction declined. Insufficient funds. Deposit some money first.')
           withdraw(self, balance, amount)
       else:
           return balance
Here is me testing the script in my Python REPL:

$ bpython
bpython version 0.22.1 on top of Python 3.10.2 /usr/bin/python
>>> import script
>>> BA = script.BankAccount('Winston', 'Smith')
>>> BA.first_name
'Winston'
>>> BA.last_name
'Smith'
>>> BA.deposit(starting_balance, 100)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    BA.deposit(starting_balance, 100)
NameError: name 'starting_balance' is not defined
>>> BA.deposit(0.00, 100.0)
100.0
>>> BA.withdraw(balance, 25.00)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    BA.withdraw(balance, 25.00)
NameError: name 'balance' is not defined
>>> BA.withdraw(100.00, 25.00)
75.0
>>> BA.withdraw(100.00, 125.00)
-25.0
>>> 
There are more things that don’t work than what does work.

Here is what works:
  • I’m able to import the script and instantiate the class by passing in a first and last name.
  • When I invoke the deposit() method including a $0.00 balance and a $100.00 bill, the method returns the new $100.0 balance.
  • When I invoke the withdraw() method and pass in the $25.00 amount, the method returns $75.00.

Here is what doesn’t work:
  • For starters the global constant class attribute variable starting_balance is defined as $0.00 so I would think that the default value of $0.00 can be passed into the deposit() method as the first argument. When I try doing this, Python says it’s not defined (when it clearly is). So anyways, to proceed I pass in $0.00.
  • When I attempt to withdraw $25.00 from the new balance, Python says “balance is not defined” even though the balance was returned in the previous deposit() transaction. It’s as if the balance value is not retained. In order to proceed, I pass in the balance of $100.00 and withdraw $25.00 which successfully returns $75.00
  • But when I proceed to withdraw $125.00 (amount greater than available balance), Python returns $-25.00 which I tried to prevent and catch with the try/exception mechanism. So this doesn’t work as intended.

My questions are:
  1. How do I declare the global constant class attribute variable properly so that it is 0.00 and don’t have to redundantly enter 0.00 when invoking deposit()?
  2. How do I retain the balance attribute after the deposit() method is called so that I can pass it along when I later attempt to withdraw from that same balance?
  3. How would you better formulate the try/except mechanism to catch transactions which don’t allow the user to go below $0.00?

This script is part of a non-credit exercise for Fred Baptiste’s Udemy course, Python Object Oriented Programming Deep Dive. I’ve also leveraged resources online such as: "What do __init__ and self do in Python?" and "Python Class Attributes: An Overly Thorough Guide".
Reply
#2
(Mar-09-2022, 01:08 PM)Drone4four Wrote: How would you better formulate the try/except mechanism to catch transactions which don’t allow the user to go below $0.00?

To answer my own question, this seems to be an improvement and puts me closer to getting my ATM machine to behave as intended:

    def withdraw(self, balance, amount):
        if (balance - amount) <= 0:
            raise ValueError('Transaction declined. Insufficient funds. Deposit some money first.')
            withdraw(self, balance, amount)
        else:
            return balance - amount
Reply
#3
You are writing class methods like they are functions.
You need to make use of self, the balance state can be kept as an instance attribute.
class BankAccount:
    def __init__(self, first_name, last_name, starting_balance=0.00):
        self.first_name = first_name
        self.last_name = last_name
        self.balance = starting_balance

    def deposit(self, amount):
        self.balance += amount
        return self.balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(
                "Transaction declined. Insufficient funds. Deposit some money first."
            )
        self.balance -= amount
        return self.balance


bank_account = BankAccount("Winston", "Smith")
print(bank_account.first_name)
print(bank_account.last_name)
print(bank_account.deposit(100))
print(bank_account.withdraw(25.00))
bank_account.withdraw(125.00)
Output:
Winston Smith 100.0 75.0
Error:
ValueError: Transaction declined. Insufficient funds. Deposit some money first.
Drone4four likes this post
Reply
#4
As Yoriz shows, your class shouldn't just be a collection of functions. Classes should have both variable and method attributes. It is reasonable that the account balance should be an attribute of account. Withdrawals and deposits change the balance. I disagree slightly with Yoriz' in implementation. I would not have a withdrawal or deposit return a balance. And I would write a repr to aid in debugging and testing.
class BankAccount:
    def __init__(self, first_name, last_name, balance=0.00):
        self.first_name = first_name
        self.last_name = last_name
        self.balance = balance
 
    def deposit(self, amount):
        self.balance += amount
 
    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(
                "Transaction declined. Insufficient funds. Deposit some money first."
            )
        self.balance -= amount

    def __repr__(self):
        """Return a string that represents the account."""
        return f"{self.__class__.__name__}({self.last_name}, {self.first_name}, balance={self.balance})"

account = BankAccount("John", "Smith", 100.00)
print(account)
account.deposit(25)
print(account)
account.withdraw(75)
print(account)
account.withdraw(60)
Output:
BankAccount(Smith, John, balance=100.0) BankAccount(Smith, John, balance=125.0) BankAccount(Smith, John, balance=50.0)
Error:
Traceback (most recent call last): File "...", line 27, in <module> account.withdraw(60) File "...", line 12, in withdraw raise ValueError( ValueError: Transaction declined. Insufficient funds. Deposit some money first.
I noticed something odd in your code. You do deposits like this:
balance = starting_balance + amount
But for a withdrawal you do this:
       try:
           balance = balance - amount
Is this not understanding how to raise an exception for a negative balance, or are you protecting against amount not being a number? If the latter, shouldn't you do the same for deposits?
Drone4four likes this post
Reply
#5
Tests are a better way than printing to check your code works. Why better? Because they'll fail if you break something - that way, you're relying on the computer to help you than a human checking the output.

Here are some example test cases:

import unittest

from bank_account import BankAccount

class TestBankAccount(unittest.TestCase):
    def test_a_deposit_increases_the_balance(self):
        account = BankAccount("Jane", "Smith")

        account.deposit(40)

        self.assertEqual(account.balance, 40)

    def test_withdrawing_an_amount_greater_than_the_balance_fails(self):
        account = BankAccount("John", "Smith", starting_balance=0)
        withdrawl_amount = 15
        
        self.assertRaises(ValueError, account.withdraw, withdrawl_amount)

if __name__ == "__main__":
    unittest.main()
Given the BankAccount in Yoriz's post, if I take out the check in withdraw for the amount being greater than the balance, running these gives:

Output:
> python3 test_bank_account.py .F ====================================================================== FAIL: test_withdrawing_an_amount_greater_than_the_balance_fails (__main__.TestBankAccount) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_bank_account.py", line 17, in test_withdrawing_an_amount_greater_than_the_balance_fails self.assertRaises(ValueError, account.withdraw, withdrawl_amount) AssertionError: ValueError not raised by withdraw ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1)
See the docs for the unittest module here.
Drone4four likes this post
Reply
#6
Thank you both for sharing your these working scripts which are instructive.

I like how cleaner deanhystad's script looks with the return statements. The __repr__ is helpful too.

Quote:Is this not understanding how to raise an exception for a negative balance, or are you protecting against amount not being a number? If the latter, shouldn't you do the same for deposits?

I was not trying protect against strings being passed in, nor ensuring only numerical (integer/float/Decimal) input. The way I formatted my exception was just a misunderstanding of how to properly raise them. Using a conditional as you two have used them makes much more sense.

I have incorporated all of your suggestions into the latest iteration of my script. I am taking my script to the next level with added features and more questions which I will follow up in a new thread later this morning.

Thanks again goes out to Yoriz and deanhystad.
Reply
#7
(Mar-10-2022, 07:37 AM)ndc85430 Wrote: Tests are a better way than printing to check your code works. Why better? Because they'll fail if you break something - that way, you're relying on the computer to help you than a human checking the output.

As a matter of fact the instructor for this Udemy course I am taking mentions unit tests in passing at the end of the module that I am working on right now. The instructor says that unit tests will come up again later in the course with a whole section devoted to it.

ndc85430: I appreciate the time and care you put into writing this unit test tailored for my test case which clearly took quite a bit of effort on your part.
Reply
#8
I’m working on a script which emulates a basic bank account that can deposit and withdraw money like an ATM. I made a forum thread earlier which was helpful but since then I’ve added new features such as an interest calculator as well as a date and time register. Taking my script to the next level, what I am struggling with is assembling all the unique information for each transaction to generate into a “confirmation number”. This is the task I am working on now.

In greater detail, here is a snippet of the description from the exercise provided by the instructor for a non-credit as a hobby Udemy course:

Quote:each deposit and withdrawal must generate a confirmation number composed of:
  • the transaction type: D for deposit, and W for withdrawal, I for interest deposit, and X for declined (in which case the balance remains unaffected)
  • the account number
  • the time the transaction was made, using UTC
  • an incrementing number (that increments across all accounts and transactions)
  • for (extreme!) simplicity assume that the transaction id starts at zero (or whatever number you choose) whenever the program starts
  • the confirmation number should be returned from any of the transaction methods (deposit, withdraw, etc)

So for example, the confirmation should look something like this:
D-140568-20190315145900-124

...where:
  • “D” represents deposit
  • ”140568” represents the account number (which is arbitrary)
  • ”20190315145900” represents a concatenated local timestamp (Specific timezone: MST)
  • “124” is the transaction ID. The instructor specifies it starts at zero and with each transaction, it should increment by 1.

That is the breakdown and structure of the confirmation number that I am trying to have my script generate for each transaction.

Here is my script:
from datetime import datetime
from pytz import timezone
 
class TimeZone:
 
   def __init__(self, locality='US/Mountain'):
       self.tz = datetime.now(timezone(locality))
       self.readable_format = '%Y-%m-%d %H:%M:%S %Z%z'
       print(f'The date and time: {self.tz.strftime(self.readable_format)}')
       self.transaction_time_id_format = '%Y%m%d%H%M%S'
       print(f'The date and time condensed: {self.tz.strftime(self.transaction_time_id_format)}')
 
class Account:
  
   interest = 0.005 # Percent
 
   def __init__(self, first_name, last_name, account_num=1400, starting_balance=0.00):
       self.first_name = first_name
       self.last_name = last_name
       self.full_name = f'{first_name} {last_name}'
       self.account_num = account_num
       self.balance = starting_balance
       self.transaction_id = 0
 
   def deposit(self, amount):
       self.balance += amount
       print(f'D-{self.account_num}-{TimeZone(self.transaction_time_id_format)}-{self.transaction_id+1}') # problem line
  
   def withdraw(self, amount):
       if amount > self.balance:
           raise ValueError('Transaction declined. Insufficient funds. Please deposit some more $$$ first.')
       self.balance -= amount
 
   def pay_interest(self):
       monthly_rate = self.interest/12
       monthly_sum = monthly_rate * self.balance
       return monthly_sum + self.balance
 
   def __repr__(self):
       """Return a string that represents the account."""
       return f"{self.__class__.__name__}({self.last_name}, {self.first_name}, balance={self.balance})"
Here is me playing around with my classes in my REPL with output:
$ bpython
bpython version 0.22.1 on top of Python 3.10.2 /usr/bin/python
>>> import script
>>> time_instance = script.TimeZone()
The date and time: 2022-03-09 22:58:27 MST-0700
The date and time condensed: 20220309225827
Above I am instantiating the TimeZone class. As you can see with the print statements, it’s working so far.

But when I attempt to instantiate the Account class and perform a deposit(), I get a traceback directing my attention to line 27:

>>> BA = script.Account('Winston', 'Smith')
>>> BA.deposit(100)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    BA.deposit(100)
  File "/home/<user>/dev/projects/python/2018-and-2020/Udemy-Fred-Baptiste-OOP/script.py", line 27, in deposit
    print(f'D-{self.account_num}-{TimeZone(self.transaction_time_id_format)}-{se
lf.transaction_id+1}')
AttributeError: 'Account' object has no attribute 'transaction_time_id_format'
At line 27 I am trying to create the confirmation number using an f-string. The problem is clearly with the way I am calling the TimeZone class embedded within the Account class within the deposit() method.

So my first question is: How do I properly interpolate the transaction_time_id_format call (with a ‘now’ timestamp for the MST time zone) from the TimeZone class into the deposit() method that exists in the other Account class?

For my humble purposes here right now with this thread, I am first just trying to build a basic f-string with the matching format of the confirmation number embedded within the method. Although I realize that I am kind of missing the point and have mostly strayed away from the larger picture that the instructor is asking for.

What the instructor really wants is another completely separate method elsewhere in the script that is likely primarily responsible for generating confirmation numbers which can then print transaction details individually or in aggregate. That is probably the end game. But at this point it’s not clear to me what that next step might be in terms of writing an outside mechanism to be triggered during every deposit() / withdrawal() / pay_interest() method call to generate confirmation numbers.

To assist with this next level challenge, any further hints, advice, guidance, and tips are welcome. Dance
Reply
#9
Trying to be vauge here wile giving you something to look into, I want to help you break down the error

I made two observations, You have defined two classes, one for Account and one for TimeZone, you have tested TimeZone and it appears successful when you instantiate it in the python repl. Good job so far. Now the traceback says
AttributeError: 'Account' object has no attribute 'transaction_time_id_format'
So lets try to understand that. You have already isolated which line we are arguing about. Account object, OK so that's our instance stored in BA . Now lets look at what the computer might be thinking, overly simplified:

print(- I need to print something to output
f'D--.........' -- ok so its a format string
{self.account_num} -- does BA have an account_num property? .. ah yes its there (was declared inside __init__)
{TimeZone(self.transaction_time_id_format)} -- so I need to instantiate a TimeZone with ...
self.transaction_time_id_format -- but BA does not define a transaction_time_id_format that's not in scope

And this is the problem. As yourself what is self what does it refer to. From which object is it going to get transaction_time_id_format from?

I don't want to write code here or answer your question directly but I would suggest reading more about self and scoping. Every object instance gets its own.
Apologetic Canadian, Sorry about that eh?
Reply
#10
If I am not mistaken, I believe the self is a reference to the instance of the class being called, as I was doing in my REPL with the BA. Although the Udemy instructor also clarified that self is mostly a necessary convention.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020