6
\$\begingroup\$

I'm learning python and I have written a bank program incorporating all the skills I've gathered so far. Using the program, one can create user profiles (name, password, account balance) which will be stored in three different text files. One can then do the normal bank transactions: deposit, withdraw, check balance or even close and delete their entire bank profile. Every transaction is accompanied by a password check.

(There are two python files: Bank.py, filestore.py and three text files: cusbalfile.txt, cusnamefile.txt, cuspassfile.txt)

Bank.py

import filestore
import time
import datetime



#############################################################################
#This is the function that is called at the beginning of the program
def postbank():                                                                         ###
    print ("Welcome to PostBank, We care for you\n")                                    ###
    prompt=int(raw_input("""To open a new bank account, Press 1\n"""+                   ###                     
                        """To access your existing account & transact press 2\n"""))    ###
    if prompt==1:                                                                       ###
        cus=BankAccount()#creates a new customer profile                                ###
    elif prompt==2:                                                                     ###
        cus=ReturnCustomer()#checks for existing customer                               ###
    else:                                                                               ###
        print "You have pressed the wrong key, please try again"                        ###
        postbank()                                                                      ###
###########################################################################################




##class for creating an instance of a new back account and other default bank functions
class BankAccount:
    """Class for a bank account"""
    type="Normal Account"
    def __init__(self):
        ##calls functions in the module filestore
        self.username, self.userpassword, self.balance=filestore.cusaccountcheck()
        print ("Thank you %s, your account is set up and ready to use,\n a 100 pounds has been credited to your account" %self.username)
        time.sleep(2)
        self.userfunctions()



    def userfunctions(self):
        print("\n\nTo access any function below, enter the corresponding key")
        print ("""To:
check Balance, press B.
deposit cash,  press D.
withdraw cash, press W.
Delete account press X.
exit service,  press E\n
:"""),
        ans=raw_input("").lower()
        if ans=='b':
            ##passcheck function confirms stored password with user input
            self.passcheck()
            self.checkbalance()
        elif ans=='d':
            self.passcheck()
            self.depositcash()
        elif ans=='w':
            self.passcheck()
            self.withdrawcash()
        elif ans=='x':
            print ("%s, your account is being deleted"%self.username)
            time.sleep(1)
            print ("Minions at work")
            time.sleep(1)
            filestore.deleteaccount(self.username)
            print ("Your account has been successfuly deleted, goodbye")
        elif ans=='e':
            print ("Thank you for using PostBank Services")
            time.sleep(1)
            print ("Goodbye %s" %self.username)
            exit()

        else:
            print "No function assigned to this key, please try again"
            self.userfunctions()

    def checkbalance(self):
        date=datetime.date.today()
        date=date.strftime('%d-%B-%Y')
        self.working()
        print ("Your account balance as at {} is {}").format(date, self.balance)
        self.transact_again()

    def withdrawcash(self):
        amount=float(raw_input("::\n Please enter amount to withdraw\n: "))
        self.balance-=amount
        self.working()
        print ("Your new account balance is %.2f" %self.balance)
        print ("::\n")
        filestore.balupdate(self.username, -amount)
        self.transact_again()

    def depositcash(self):
        amount=float(raw_input("::\nPlease enter amount to be deposited\n: "))
        self.balance+=amount
        self.working()
        print ("Your new account balance is %.2f" %self.balance)
        print ("::\n")
        filestore.balupdate(self.username, amount)
        self.transact_again()



    def transact_again(self):
        ans=raw_input("Do you want to do any other transaction? (y/n)\n").lower()
        self.working()
        if ans=='y':
            self.userfunctions()
        elif ans=='n':
            print ("Thank you for using PostBank we value you. Have a good day")
            time.sleep(1)
            print ("Goodbye {}").format(self.username)
            exit()
        elif ans!='y' and ans!='n':
            print "Unknown key pressed, please choose either 'N' or 'Y'"
            self.transact_again()


    def working(self):
        print("working"),
        time.sleep(1)
        print ("..")
        time.sleep(1)
        print("..")
        time.sleep(1)


    def passcheck(self):
        """prompts user for password with every transaction and counterchecks it with stored passwords"""
        b=3
        while b>0:
            ans=raw_input("Please type in your password to continue with the transaction\n: ")
            if ans==self.userpassword:
                return True


            else:
                print "That is the wrong password"
                b-=1
                print ("%d more attempt(s) remaining" %b)

        print ("Account has been freezed due to three wrong password attempts,\n contact your bank for help, bye bye")
        time.sleep(1)
        print ("...")
        time.sleep(1)
        print("...")
        time.sleep(1)

        exit()


class ReturnCustomer(BankAccount):
    type="Normal Account"
    def __init__(self):
        self.username, self.userpassword, self.balance=filestore.oldcuscheck()
        self.userfunctions()

postbank() ##calling the function to run the program

filestore.py

##creating empty lists everytime the program is initialized
cusnames=[]
cuspasswords=[]
cusbalance=[]

##opening the storage files to collect old customer data
namefile=open("cusnamefile.txt", "r")
passfile=open("cuspassfile.txt", "r")
balfile=open("cusbalfile.txt", "r")

##populate the empty lists with data from storage files
##check list of customer names
for line in namefile:
        cusnames.append(line[:-1])
namefile.close()

##check list of customer passwords
for line in passfile:
        cuspasswords.append(line[:-1])
passfile.close()

##check list of customer balances
for line in balfile:
        cusbalance.append(line[:-1])
balfile.close()


##function creates a new user 
def cusaccountcheck():
        name=""
        pin=""

        while name not in cusnames and len(name)<3:
                name=raw_input("Please type in your name for this new bank account\n")
                if name not in cusnames:
                        cusnames.append(name)
                        filewrite(cusnames)
                        break
                print("Sorry, that user name is already in use")
                ans=raw_input("Are you already a member at this bank? (y/n)\n")
                if ans.lower()=='y':
                        oldcuscheck()
                else:
                        cusaccountcheck()

        while len(pin)<4:
                pin=raw_input("Please assign a password to this account, pin should be at least 5 characters\n")
                if len(pin)>4:
                        print "your pin has been successfully saved"
                        print "Remember to always keep your pin safe and don't disclose it to anybody"
                        cuspasswords.append(pin)
                        cusbalance.append(0)
                        balance=100.0
                        cusbalance[cusnames.index(name)]=balance
                        filewrite(cuspasswords)
                        filewrite(cusbalance)
                        break

                print ("Sorry, that is a short password")

        return name,pin, balance

##Function to check returning customer
def oldcuscheck():
        name=""
        while name not in cusnames:
                name=raw_input("What is your name?\n")
                if name in cusnames:
                        username=name
                        userpassword=cuspasswords[cusnames.index(name)]
                        balance=float(cusbalance[cusnames.index(name)])
                        return username, userpassword, balance
                else:
                        print ("Sorry %s, It looks like you didn't spell you name correctly or your name is not in our records"%name)
                        again=raw_input("would like to type in your name again? (y/n)")
                        if again.lower()=='y':
                                oldcuscheck()
                        else:
                                print ("Bye bye, thank you for trying Postbank")
                                exit()




##This function writes new data into the storage files whenever called upon.      
def filewrite(item):
        if item==cusnames:
                text=open("cusnamefile.txt","w")
                for i in item:
                        text.write(i+"\n")
                text.close()

        elif item==cuspasswords:
                text=open("cuspassfile.txt", "w")
                for i in item:
                        text.write(i+"\n")
                text.close()

        elif item==cusbalance:
                text=open("cusbalfile.txt", "w")
                for i in item:
                        text.write(str(i)+"\n")
                text.close()

###This function updates the account balance after a withdraw or deposit transaction
def balupdate(ind, amount):
        accountnumber=cusnames.index(ind)
        accountbal=float(cusbalance[accountnumber])
        accountbal+=amount
        cusbalance[accountnumber]=accountbal
        text=open("cusbalfile.txt", "w")
        for i in cusbalance:
                text.write(str(i)+"\n")
        text.close()

###This function deletes an existing account and any data that was stored about it is cleared
def deleteaccount(name):
        accountnumber=cusnames.index(name)
        del cusnames[accountnumber]
        filewrite(cusnames)
        del cusbalance[accountnumber]
        filewrite(cusbalance)
        del cuspasswords[accountnumber]
        filewrite(cuspasswords)
        return None

cusbalfile.txt

450693.0
6782449.0
3525.0
6000000.0
5532.0

cusnamefile.txt

Loise Njogu
Moses Njagi
MakerFuse Limited
Dad
Dennis Meyer

cuspassfile.txt

12345
09876
makerpassword
thedadpassword
234234
\$\endgroup\$
8
\$\begingroup\$

Separation of concerns

Also known as the single responsibility principle. Perhaps the biggest issue I see in this post is that the account management logic (deposit, withdraw, others) is mixed with handling keyboard input and printing output.

Take this function for example:

def withdrawcash(self):
    amount=float(raw_input("::\n Please enter amount to withdraw\n: "))
    self.balance-=amount
    self.working()
    print ("Your new account balance is %.2f" %self.balance)
    print ("::\n")
    filestore.balupdate(self.username, -amount)
    self.transact_again()

Too many things are happening here:

  • Reading from keyboard
  • Updating balance in memory
  • Updating balance in a file, via reference to external object filestore
  • Printing
  • Recursive call to user interface loop

But actually it's a bit late to separate concerns here. They should have been separated at the class design level. These actions don't belong in a BankAccount class.

Consider this alternative organization of classes:

  • Bank: in charge of bank accounts in the bank, and storage
  • BankAccount: in charge of managing the balance of an account
    • Has a reference to Bank
  • BankApp: in charge of the interactive loop, reading input, printing output, and calling
    • Has a reference to Bank and Account

The interactive BankApp could look more like this:

class BankApp:
    # ...

    def main(self):
        while True:
            if action == ACTION_QUIT:
                break
            if action == ACTION_WITHDRAW:
                self.handle_withdraw()
            # ...

    def handle_withdraw(self):
        # print welcome

        # input amount

        if self.bank.withdraw(amount):
            # print success
        else:
            # print failure

Key points about the handle_withdraw method:

  • Doesn't manipulate the balance directly, it delegates that task to self.bank.withdraw
  • Doesn't need to know where the balance is stored. That's a job for self.bank.withdraw. It could be in memory, in file, on the moon, or all of these. It's not the responsibility of the interactive app to know such implementation details
  • Doesn't control the user interface loop: it handles a one-time withdraw action and returns. Awareness of the user interface loop (of repeated transactions with the bank) is not necessary at this level
  • Consider that the withdrawal might not succeed. My bank certainly won't cash out one million bucks for me

This example is incomplete, but I hope it helps you get started.

If you can get your hands on the book Code Complete, chapter 6 about class design and Abstract Data Types will give you great insight, and naturally drive you towards better separation of concerns and more flexible design.

Working with files

This way of working with files is error prone and not recommended:

namefile=open("cusnamefile.txt", "r")
# ...
for line in namefile:
        cusnames.append(line[:-1])
namefile.close()

The recommended technique:

with open("cusnamefile.txt", "r") as namefile:
    for line in namefile:
        cusnames.append(line[:-1])

Notice that I didn't close namefile. I didn't need to, the with block takes care of that.

By the way, since reading is the default mode, you can drop that "r".

Using list comprehensions

Instead of:

cusnames=[]

with open("cusnamefile.txt") as namefile:
    for line in namefile:
        cusnames.append(line[:-1])

List comprehensions are short and sweet and powerful:

with open("cusnamefile.txt") as namefile:
    cusnames = [line[:-1] for line in namefile]

Self-inflicted pain?

What's up with the trailing ### on these lines?

#This is the function that is called at the beginning of the program
def postbank():                                                                         ###
    print ("Welcome to PostBank, We care for you\n")                                    ###
    prompt=int(raw_input("""To open a new bank account, Press 1\n"""+                   ###                     
                        """To access your existing account & transact press 2\n"""))    ###
    if prompt==1:                                                                       ###
        cus=BankAccount()#creates a new customer profile                                ###
    elif prompt==2:                                                                     ###
        cus=ReturnCustomer()#checks for existing customer                               ###
    else:                                                                               ###
        print "You have pressed the wrong key, please try again"                        ###
        postbank()                                                                      ###
###########################################################################################

Although they are nicely aligned, consider what will happen when you will need to change the code of these lines: that's right, you may need to add and remove spaces to keep the ### aligned. Whose value is highly questionable. Probably these cause you more pain than benefit, I suggest to delete these. In general, any unnecessary stuff hurts you more in the long run that it helps.

Where is this PostBank anyway?

class BankAccount:
    def __init__(self):
        # ...
        print ("Thank you {username}, your account is set up and ready to use,".format(username=self.username)
            + "\n" + "a 100 pounds has been credited to your account")

I'd like to open a few dozen accounts and then withdraw 100 pounds from each ;-)

\$\endgroup\$
8
\$\begingroup\$

A few things to point out:


#############################################################################
#This is the function that is called at the beginning of the program

Please do not do this. Ever.

You don't need to wrap your initial code in a big, literal box.

A simple comment, or even multi-line comment like the following would be much better:

#This is the initial function, it does XYZ process.

"""This is the initial function, it does XYZ process.
   Usage:
       - sumOf(firstNumber, secondNumber)
       For example; the following:
       - sumOf(2, 3) should return 5
"""

Building on the last point, those """ are reserved for docstrings, which means you shouldn't be using them in functions, which means the following needs to go:

print ("Welcome to PostBank, We care for you\n")                                    ###
prompt=int(raw_input("""To open a new bank account, Press 1\n"""+                   ###                     
                    """To access your existing account & transact press 2\n"""))

Also, you should seperate the new-line characters into new strings and just concatenate them in. This makes for much clearer readability:

print("Welcome to PostBank, We care for you" + "\n")
prompt = int(raw_input("To open a new bank account, Press 1" + "\n"
                     + "To access your existing account & transact press 2:" + "\n"))

Additionally, you should move the opening message to a constant, that way, when/if you want to change/improve it, you can do it much more easily.


There's a few things to point out about this:

if prompt==1:                                                                       ###
    cus=BankAccount()#creates a new customer profile                                ###
elif prompt==2:                                                                     ###
    cus=ReturnCustomer()#checks for existing customer                               ###
else:                                                                               ###
    print "You have pressed the wrong key, please try again"                        ###
    postbank()   

Instead of recursively calling the function, consider a while(true) loop instead.

while(true):
    prompt = int(raw_input("To open a new bank account, Press 1" + "\n"
                         + "To access your existing account & transact press 2:" + "\n"))
    if prompt==1:
        cus = BankAccount() #creates a new customer profile
    elif prompt==2:
        cus = ReturnCustomer() #checks for existing customer
    else:
        print "You have pressed the wrong key, please try again"  

Let's improve that a bit further with use of a dictionary:

prompts = {
    1: BankAccount,   # Creates a new customer profile
    2: ReturnCustomer # Checks for existing customer
}
while(true):
    prompt = int(raw_input("To open a new bank account, Press 1" + "\n"
                         + "To access your existing account & transact press 2:" + "\n"))
    if prompt in prompts:
        prompts[prompt]()
    else:
        print "You have pressed the wrong key, please try again"

Note that cus wasn't assigned, because it does nothing.


class BankAccount:
    """Class for a bank account"""
    type="Normal Account"
    def __init__(self):
        ##calls functions in the module filestore
        self.username, self.userpassword, self.balance=filestore.cusaccountcheck()
        print ("Thank you %s, your account is set up and ready to use,\n a 100 pounds has been credited to your account" %self.username)
        time.sleep(2)
        self.userfunctions()
  • time.sleep(2): No. Remove it. (they're nearly useless, and certainly so in this context)
  • Use some whitespace between your operators. Read over Python's official style guide, PEP8 to see all the rules one must follow when writing correct Python.
  • instead of %s and %self.username use the "{username}".format(username=self.username) like format, it certainly improves readability, and removes the need for an explicit order.
  • Declare the variables on their own line: it's much better on readability to do so.
  • Your functions and variables should have undercases instead of nospace. See above point about PEP8
  • type goes nowhere, kill it. Useless/unused variables are redundant.
# Class for creating an instance of a new back account and other default bank functions
class BankAccount:
    def __init__(self):
        # Calls functions in the module filestore
        self.user_name
        self.user_password
        self.balance = file_store.cus_account_check()
        print ("Thank you {username}, your account is set up and ready to use,".format(username=self.username)
            + "\n" + "a 100 pounds has been credited to your account")
        self.user_functions()

Applying those changes globally, a refactored block would look like:

import filestore
import time
import datetime

WELCOME_MESSAGE = "Welcome to PostBank, We care for you" + "\n"
def postbank():
    print (WELCOME_MESSAGE)
    prompts = {
        1: BankAccount,   # Creates a new customer profile
        2: ReturnCustomer # Checks for existing customer
    }
    while True:
        prompt = int(raw_input("To open a new bank account, Press 1" + "\n"
                             + "To access your existing account & transact press 2:" + "\n"))
        if prompt in prompts:
            prompts[prompt]()
        else:
            print "You have pressed the wrong key, please try again"


# Class for creating an instance of a new back account and other default bank functions
class BankAccount:
    def __init__(self):
        ##calls functions in the module filestore
        self.user_name
        self.user_password
        self.balance = filestore.cus_account_check()
        print ("Thank you {username}, your account is set up and ready to use,".format(username=self.username)
            + "\n" + "a 100 pounds has been credited to your account")
        self.user_functions()


    def user_functions(self):
        print("\n\nTo access any function below, enter the corresponding key")
        print ("To:" + "\n"
            + "Check Balance, press B" + "\n"
            + "Deposit cash:  press D" + "\n"
            + "Withdraw cash, press W" + "\n"
            + "Delete account press X" + "\n"
            + "Exit service,  press E" + "\n")

        functions = {
            'b': self.check_balance,
            'd': self.deposit_cash,
            'w': self.withdraw_cash,
        }
        while True:
            answer = raw_input("> ").lower()
            if answer in functions:
                ##passcheck function confirms stored password with user input
                self.pass_check()
                functions[answer]()
            elif answer is 'x':
                print ("{username}, your account is being deleted".format(username=self.username))
                file_store.delete_account(self.username)
                print ("Your account has been successfuly deleted, Goodbye.")
            elif answer is 'e':
                print ("Thank you for using Dot Inc Bank Services")
                print ("Goodbye {username}".format(username=self.username))
                exit()
            else:
                print "No function assigned to this key, please try again"

    def check_balance(self):
        date = datetime.date.today().strftime('%d-%B-%Y')
        print ("Your account balance as at {time} is {balance}").format(time=date, balance=self.balance)
        self.transact_again()

    def withdraw_cash(self):
        amount = float(raw_input("Please enter amount to withdraw:" + "\n"))
        self.balance -= amount
        print ("Your new account balance is {balance}".format(balance=self.balance) + "\n")
        file_store.balance_update(self.username, -amount)
        self.transact_again()

    def deposit_cash(self):
        amount = float(raw_input("Please enter amount to deposit:" + "\n"))
        self.balance += amount
        print ("Your new account balance is {balance}".format(balance=self.balance) + "\n")
        file_store.balance_update(self.username, -amount)
        self.transact_again()



    def transact_again(self):
        while True:
            answer = raw_input("Do you want to do another transaction? (y/n)" + "\n").lower()
            if ans is 'y':
                self.user_functions()
            elif ans is 'n':
                print ("Thank you for using PostBank we value you. Have a good day.")
                print ("Goodbye {username}").format(username=self.username)
                exit()
            elif ans is not in ['y', 'n']:
                print "Unknown key pressed, please choose either 'N' or 'Y'"


    def pass_check(self):
        # Prompts user for password with every transaction and counterchecks it with stored passwords"
        attempts = 3
        while attempts > 0:
            answer = raw_input("Please type in your password to continue with the transaction:" + "\n")
            if answer is self.user_password:
                return True
            else:
                print "That is the wrong password"
                attempts -= 1
                print ("{attempts} more attempt(s) remaining".format(attempts=attempts))

        print ("Account has been frozen due to three wrong password attempts," + "\n"
            + "Contact your bank for help.")
        exit()


class ReturnCustomer():
    def __init__(self):
        self.username
        self.user_password
        self.balance = file_store.old_customer_check()
        self.user_functions()

postbank()

File system:

Instead of using files for each listing, and seperating each file by line, consider using JSON instead (with a single file).

By using JSON, you can easily serialise the data into dictionaries which can be easily accessed:

A theoretical JSON structure for you may look like:

"users": {
    "Daynisdot": {
        "password": "python_rox_my_sox.1"
         "balance": 1000000
         "is-locked": false
    }
    "Quill": {
        "password": "featherpen.5"
        "balance": 5.99
        "is-locked": true
    }
}

Note that I used the usernames as keys, which makes lookup much easier.

Also note that you could add the is-locked field to match the usage of pass_check function.

\$\endgroup\$
  • 2
    \$\begingroup\$ """ are reserved for docstrings and should not be used for multi-line strings? Do you have any other source for this suggestion? Multiline strings are quite handy to use, I don't know why I should stop using them. \$\endgroup\$ – sebix Sep 5 '15 at 15:59
  • \$\begingroup\$ @sebix: see [PEP8]python.org/dev/peps/pep-0008/#documentation-strings) \$\endgroup\$ – Quill Sep 5 '15 at 16:05
  • \$\begingroup\$ @Quill PEP 257 says that we should use triple-quoted strings for docstrings, but I don’t see anything forbidding their use for other strings. Indeed, the PEP 8 section on String Quotes specifically mentions tripled-quoted strings that aren’t docstrings. \$\endgroup\$ – alexwlchan Sep 5 '15 at 17:37
  • 1
    \$\begingroup\$ It doesn’t affect the main point (use a while loop instead of recursive calls), but while(true): will throw a NameError in Python. It should be while True:. \$\endgroup\$ – alexwlchan Sep 5 '15 at 17:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.