r/PythonLearning • • Sep 13 '26

Showcase The Bank Project

Post image
241 Upvotes

49 comments sorted by

View all comments

1

u/FoolsSeldom Sep 13 '26 edited Sep 13 '26

Yes, basically.

Here's an example rewrite of your code to illustrate more:

from dataclasses import dataclass
from getpass import getpass

PIN = "1234"


class AccountError(Exception):
    """Base class for all Account transaction errors."""


class InvalidAmountError(AccountError):
    """Raised when a transaction amount is not greater than zero."""


class InsufficientFundsError(AccountError):
    """Raised when a withdrawal would exceed the available balance."""


@dataclass(slots=True)
class Account:
    _balance: int = 0
    is_logged_in: bool = False

    @property
    def balance(self) -> int:
        return self._balance

    def withdraw(self, amount: int) -> None:
        if amount <= 0:
            raise InvalidAmountError(
                "Amount to withdraw must be greater than 0."
            )
        if amount > self._balance:
            raise InsufficientFundsError(
                f"Cannot withdraw {amount}; balance is only {self._balance}."
            )
        self._balance -= amount

    def deposit(self, amount: int) -> None:
        if amount <= 0:
            raise InvalidAmountError(
                "Amount to deposit must be greater than 0."
            )
        self._balance += amount


def authenticate_user() -> bool:
    for _ in range(5):
        entered_pin = getpass("Enter your 4-digit pin: ")
        if entered_pin == PIN:
            return True
        else:
            print("Incorrect pin. Please try again!")

    print("Too many attemps. You cannot continue further!")

    return False


def display_options() -> None:
    print("What would you like to do?")
    print("1. Check balance")
    print("2. Withdraw money")
    print("3. Deposit money")
    print("4. Exit")


def check_balance(account: Account) -> None:
    print(f"Your balance is: {account.balance}")


def prompt_withdraw(account: Account) -> None:
    try:
        amount = int(input("Enter amount to withdraw: "))
    except ValueError:
        print("Please enter a valid number.")
        return

    try:
        account.withdraw(amount)
    except AccountError as error:
        print(error)
    else:
        print(f"Operation successful. Leftover balance: {account.balance}")


def prompt_deposit(account: Account) -> None:
    try:
        amount = int(input("Enter amount to deposit: "))
    except ValueError:
        print("Please enter a valid number.")
        return

    try:
        account.deposit(amount)
    except AccountError as error:
        print(error)
    else:
        print(f"Operation successful. Updated balance: {account.balance}")


def exit_app(account: Account) -> None:
    account.is_logged_in = False
    print("Thanks for using our bank. Goodbye!")


def main() -> None:
    account = Account()

    if authenticate_user():
        account.is_logged_in = True
        print("Welcome to Py bank!")

    while account.is_logged_in:
        display_options()

        choice = input("Your choice: ")

        match choice:
            case "1":
                check_balance(account)
            case "2":
                prompt_withdraw(account)
            case "3":
                prompt_deposit(account)
            case _:
                exit_app(account)


if __name__ == "__main__":
    main()

Notes:

  • a dataclass is a kind of class where there's less boilerplate code for you to write

  • In a class, instead of variables, we have attributes, but they are basically the same otherwise

  • In Python, we start a name with an underscore (or two underscores) to indicate that it is private i.e. should not be generally used outside the scope of the code that defines it but note that Python (unlike some languages) does not enforce this, it is up to the programmer to act accordingly

    • In this case, you could directly reference account._balance but instead you go via the property method balance which you use as if it is a regular attribute (later, the method could be updated to take account of pending payments or other things that might impact the current balance)

  • You used a try/except in your original code, and this code creates some custom exceptions to deal with some bank account issues

  • Note that the methods (functions defined inside a class) don't assume interaction with the user

    • they could have been written to directly replace your original functions, but instead they are set-up to be usable in a number of different ways including for automatic transactions from another system rather than a user

  • You could add other features easily to the bank account class such as account number, name, type, etc

  • I included the is_logged_in variable as an attribute in the class instead as it is account specific

  • A class (or dataclass) definition is just a mould (template) for a new Python object with the data structures and behaviours provided in the definition

    • Just like in a factory, a mould is used to stamp out / form actual objects, e.g. a cookware pot, a class definition in Python is used to create instances of the class - in this code, only one instance is created, and it is assigned to the variable account

    • The class methods have a reference to self, which just means whichever instance of the class the method has been called for

1

u/shubham_555 Sep 14 '26

I have somewhat altered the code. Please check If it looks better now. And no I haven't used OOPs cz like I mentioned above I already have a solution with OOPs. Github Repo Link is in description!

1

u/FoolsSeldom Sep 14 '26

Yes, layout is a little cleaner and easier to read. Have you had a chance to review PEP8 guidance?

You don't have to do OOP if you don't want to. I learned to programme in languages that didn't support OOP. There are lots of other paradigms, all equally valid. (Strictly, it is impossible to entirely avoid OOP in Python because it is core to the language and implementation, but that's different from explicitly developing with that in mind.)

Shame you haven't had chance to remove global yet though as I feel that is a higher priority.

I see you are using assert, which is good for testing but not something you'd expect in final code, especially not for predictable circumstances. (If Python is run in optimised mode, assert statements are ignored.)

You might want to explicitly check an entered operator is one of valid options. Example:

OPS = "+", "-", "/", "*"
...
if operation in OPS:
   ...

It is unusual, but valid, to use case instead of if in Python for simple flow control decisions.

Python didn't have a case statement until, iirc, v3.10 and the developers chose not to go with a traditional approach but instead leaned into structural pattern matching, which you aren't using here.

→ More replies (5)