4
\$\begingroup\$

I have written code for a budget calculate program.

Here is the problem :

Write a program that asks the user to enter the amount that he or she has budgeted for a month. A loop should then prompts the user to enter each of his or her expenses for the month and keep a running total. When the loop finishes, the program should display the amount that the user is over or under budget.


Here is the code I wrote:
# This program calculates budget for a month for a user.
# Get the budget of a user
budget = int(input('Enter the budget of the month: '))
another = 'y'  # Variable to control the loop

# Declare the accumulator
total = 0.0

# Process one or more expenses
while another == 'y' or another == 'Y':
    # Get the expenses
    expenses = float(input('Enter the expenses: '))
    total += expenses
    # Do this again ?
    another = input('Do you have another expenses? ' +
                    '(Enter y for yes): ')

if total > budget:
    print(f'You are ${total - budget} over the budget ')
elif total == budget:
    print(f'Your budget is enough for expenses!')
else:
    print(f'You are ${total - budget} under the budget ')

print(f'Total: ${total}')

Here is the output :

# Program Output
# Enter the budget of the month: 500
# Enter the expenses: 200
# Do you have another expenses? (Enter y for yes): y
# Enter the expenses: 600
# Do you have another expenses? (Enter y for yes): n
# You are $300.0 over the budget
# Total: $800.0

# Program Output
# Enter the budget of the month: 700
# Enter the expenses: 200
# Do you have another expenses? (Enter y for yes): y
# Enter the expenses: 150
# Do you have another expenses? (Enter y for yes): n
# You are $-350.0 under the budget
# Total: $350.0

# Program output
# Enter the budget of the month: 1000
# Enter the expenses: 500
# Do you have another expenses? (Enter y for yes): y
# Enter the expenses: 500
# Do you have another expenses? (Enter y for yes): n
# Your budget is enough for expenses!
# Total: $1000.0

The purpose of this exercise is to practice loops. Because I'm new to programming, I like to read other people's code but I haven't found anything related to this exercise. This is an exercise in the tony gaddis python book.

Can someone read this code and comment about it. Mistakes, things i need to improve. Thank you in advance.

\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

Your code works! You have done the first step which is to make your code work congratulations! How your code looks is often how my own code looks before i refactor it. Refactoring is something we should do once we get the ****** thing working. The standard way a beginner Python program should look is this

imports ...

def ... 

def ... 

if __name__ == "__name__":
    main code goes here

The definitions should do one thing and only one thing. For instance obtain the budget, ask the user if they want to continue, calculating expenses etc.

Useless comments one should really avoid useless comments as they clutter the code and does not contribute anything. For instance

# Get the budget of a user
budget = int(input('Enter the budget of the month: '))

Is just as clear without the comment. What if we decide to change the budget to include not only users but dogs cats and other felines? Than our comment is not just clutter, but even wrong! Maintaining and updating comments to be in sync with our code quickly becomes an unnecessary chore.

Your comments can be transferred into explicit typing hints and docstrings.

How to make code readable. Your code is very readable, which is a great thing! Good job! My suggestion for making code readable is as follows: 1 split the code into sensible modules (or files) where each file has a single purpose and a good name. Similarly split the file into functions with clear name, purpose and documentation. The function should then consist of variables with, you guessed it, clear, concise and succinct variable names. Comments should be reserved to explain really terse parts of the code, or sometimes explain why. I also use them for references to Stackoverflow when I borrow code.

Fragile handling of user input In my opinion your biggest mistake is fragile handling of user input. Users are not particularly clever beings, and requiring them to always provide you with perfect input is a fools errand. What happens in your code if I enter "four hundred dollars" as my budget? What happens if I enter a negative expense? What if I write in y (notice the extra trailing whitespace) when asked to continue? These are all things that should be considered.

One way to ask the user for confirmation is as follow

def confirm(text: str) -> bool:
    """Ask the user for confirmation to continue"""
    return input(f"{text} [y/N] ").strip().lower().startswith("y")

However, it is tedious for the user to have to confirm every single expense which included. A better option is to break if the input is blank. Here we can use that an empty string "" in python is interpreted as false.

Minor details

This part here in my eyes smell

if total > budget:
    print(f'You are ${total - budget} over the budget ')
elif total == budget:
    print(f'Your budget is enough for expenses!')
else:
    print(f'You are ${total - budget} under the budget ')

The problem is that the first and second line only differ by a single word, which indicates that we can refactor it. You are also computing the difference between the total and budget multiple times. Something like the following solves these problems

if (budget_diff := expenses - budget) == 0:
    print(f"Your expenses are exactly on budget!")
else:
    print(
        f"You are ${abs(budget_diff)} "
        f"{'under' if budget_diff else 'over'} the budget "
    )

code

Fixing the things above, adding functions, typing hints an if __name__ == "__main__" guard turns your code into this

"""This program calculates budget for over a given time period."""

from typing import Callable, Annotated

Budget = Annotated[
    float, "the amount the user has budgeted for the current time period"
]
Expenses = Annotated[
    float, "the total amount of expenses the user has for the current time period"
]


def user_input(text: str, condition: Callable[[str], bool], expected: str) -> str:
    """Makes sure user input satisfies condition"""
    while not condition(value := input(text).strip()):
        print(f"Ooops! Expected {expected}, but obtained {value} instead.")
    return value


def is_non_negative(number: str) -> bool:
    """Returns true if the input is a non negative integer"""
    try:
        return int(number) >= 0
    except ValueError:
        return False


def get_budget(period) -> Budget:
    """Returns the users budget for the given time period"""
    text = f"\nEnter the budget for the {period}: "
    condition = is_non_negative
    expected = "a positive value"
    return float(user_input(text, condition, expected))


def get_expenses(period) -> Expenses:
    """Returns the users total expenses for the given time period"""
    expenses = 0
    text = "[leave blank to quit] expense: "
    condition = lambda x: is_non_negative(x) or not x
    expected = "A positive value"

    print(f"\nPlease write in your expenses for the current {period}.\n")
    while expense := user_input(text, condition, expected):
        expenses += float(expense)
    return expenses


def summary(budget: Budget, expenses: Expenses, period: str) -> str:
    """Returns a summary of the budget and expenses for the period"""
    summary = []
    if (budget_diff := expenses - budget) == 0:
        summary.append(f"\nYour expenses are exactly on budget for the {period}!")
    else:
        summary.append(
            f"\nYou are ${abs(budget_diff)} "
            f"{'under' if budget_diff else 'over'} the budget for the {period}."
        )
    summary.append(f"Expenses: ${expenses}, Budget: ${budget}.")
    return "\n\n".join(summary)


if __name__ == "__main__":
    period = "month"
    budget = get_budget(period)
    expenses = get_expenses(period)
    print(summary(budget, expenses, period))
\$\endgroup\$
0
5
\$\begingroup\$

Overlapping a little with the feedback from N3buchadnezzar:

  • Move your code out of the global namespace into functions
  • Use locale to format your currencies
  • Don't use float to represent currencies; use Decimal to prevent accuracy loss
  • Consider simplifying your user interface to terminate expense entry with a blank string

Suggested

"""This program calculates budget for a month for a user."""
from decimal import Decimal, DecimalException
from locale import currency, setlocale, LC_ALL
from typing import Iterator, Optional


def try_get_decimal(as_string: str) -> Optional[Decimal]:
    try:
        value = Decimal(as_string)
        if value <= 0:
            print('Number must be positive')
        else:
            return value
    except DecimalException:
        print('Invalid decimal number')


def get_budget() -> Decimal:
    while True:
        value = try_get_decimal(input('Enter the budget of the month: '))
        if value is not None:
            return value


def get_expenses() -> Iterator[Decimal]:
    while True:
        as_string = input('Enter an expense, or press enter to continue: ')
        if as_string == '':
            break
        value = try_get_decimal(as_string)
        if value is not None:
            yield value


def main() -> None:
    setlocale(LC_ALL, '')

    budget = get_budget()
    total = sum(get_expenses())

    surplus = budget - total
    if surplus > 0:
        print(f'You are {currency(surplus)} under the budget')
    elif surplus == 0:
        print('Your budget is enough for expenses!')
    else:
        print(f'You are {currency(-surplus)} over the budget')

    print(f'Total: {currency(total)}')


if __name__ == '__main__':
    main()
\$\endgroup\$
2
  • 1
    \$\begingroup\$ get is a very meaningless word, usually used for trivial access. I would rather name the functions something like try_parse_decimal, read_budget and read_expenses \$\endgroup\$
    – Kroltan
    Dec 15, 2021 at 13:14
  • \$\begingroup\$ ...or even, since you're splitting into functions, separate concerns a bit: Have the prompts passed in via parameters, and name the user-facing functions more generally, something like read_decimal and read_decimals_until_blank. \$\endgroup\$
    – Kroltan
    Dec 15, 2021 at 13:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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