Basic ATM machine (deposits/withdrawals) using OOP

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 on Python Object Oriented Programming Deep Dive. I’ve also leveraged resources online such as: oop - What do __init__ and self do in Python? - Stack Overflow and Python Class Attributes: Examples of Variables | Toptal.

edit: formatting

Will the starting balance always be $0.00? If so, you can implement that in the __init__ method.

I chose to put the starting_balance attribute outside the dunder __init__ for two reasons:

  1. I’m storing a constant which is true every time the class is instantiated
  2. I’m defining a default value

I learned about this use case in the guide I referred to earlier titled: Python Class Attributes: Examples of Variables | Toptal®. The specific passage which outlines that reasoning can be found under the heading:“So When Should you Use Python Class Attributes?”.

To answer my own question there, 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

If the starting balance will always be $0.00, you can add this statement to the __init__ method to create the corresponding instance variable:

       self.balance = 0.00

You do not need to add another parameter to the header of the __init__ method to do this.

The deposit method does not need a starting_balance parameter. That method should instead just add amount to self.balance and save the result in self.balance to update it.

EDIT (March 9, 2022):

Some of the above also applies to the withdraw method. One of the great features of OOP is that methods have access to the instance variables. Accordingly, the withdraw method does not need a balance parameter. Instead, have it work with self.balance to access and update the instance variable.

Thank you @Quercus: I have moved the balance below the __init__. I have also removed the balance arguments from the method paramaters. My script is much improved, but still has issues.

Here is my latest attempt:

#
 
class BankAccount:
   def __init__(self, first_name, last_name):
       self.first_name = first_name
       self.last_name = last_name
       self.balance = 0.00 # USD
 
   def deposit(self, amount):
       balance = self.balance + amount
       return balance
  
   def withdraw(self, amount):
       if (self.balance - amount) <= 0:
           raise ValueError('Transaction declined. Insufficient funds. Deposit some money first.')
           withdraw(self, amount)
       else:
           return balance - amount

Here is the instantiation in my script inside a REPL:

$ bpython
bpython version 0.22.1 on top of Python 3.10.2 /usr/bin/python
>>> import script
>>> BA = script.BankAccount('Winston', 'Smith')

So far so good. When I attempt to withdraw $25, the ValueError is raised as expected:

>>> BA.withdraw(25)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    BA.withdraw(25)
  File "/home/<user>/dev/projects/python/2018-and-2020/Udemy-Fred-Baptiste-OOP/script.py", line 15, in withdraw
    raise ValueError('Transaction declined. Insufficient funds. Deposit some mon
ey first.')
ValueError: Transaction declined. Insufficient funds. Deposit some money first.

Hooray! It works so far. Now for a deposit:

>>> BA.deposit(200.00)
200.0

That works too. But then I try to add another $200 to create $400:

>>> BA.deposit(200.00)
200.0
>>> 

That doesn’t work because Python is just adding $200 to the original $0.00 balance declared beneath the dunder __init__. My new question: How do I get Python to add up a running tally and add numbers on top of each other rather than just adding to the original balance attribute 0.00?

Here is something else that doesn’t quite work as intended:

>>> BA.deposit(100.00)
100.0
>>> BA.withdraw(25)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    BA.withdraw(25)
  File "/home/<user>/dev/projects/python/2018-and-2020/Udemy-Fred-Baptiste-OOP/script.py", line 15, in withdraw
    raise ValueError('Transaction declined. Insufficient funds. Deposit some mon
ey first.')
ValueError: Transaction declined. Insufficient funds. Deposit some money first.
>>> 

After depositing $100, that should become the new balance and withdrawing $25.00 should result in $75.00. That is what I am expecting. But the REPL shows that it is trying to subtract $25.00 from the original balance of 0.00 also declared beneath the dunder __init__. How do I tell Python to update / modify the balance every time a deposit/withdrawal is made?

In the deposit method you have this:

       balance = self.balance + amount

That creates a new unnecessary local variable named balance inside the deposit method. To update the instance variable, you need to specify self.balance to the left of the assignment operator.

In the withdraw method, you have this:

           withdraw(self, amount)

The purpose of that is unclear, but it does not accomplish anything. After the ValueError is raised inside the if block, there is nothing more to do there.

In the else block that follows, you have:

           return balance - amount

Instead, what needs to be done is to update self.balance by subtracting the amount from it that is to be withdrawn. Be sure to save the result in self.balance.

Noted. I have added self. to all the mentions of balance in my script.

I have removed the misplaced and redundant withdraw(self, amount).

Here is the latest iteration of my script:

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 #USD

    def deposit(self, amount):
        self.balance += amount
    
    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(
                'Transaction declined. Insufficient funds. Please deposit some more $$$ first.'
                )
        self.balance -= amount 

It runs beautifully!

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 @Quercus for your guidance and support so far.

1 Like