Scientific Computing with Python Beta: Budget App

This is my submission for the Budget App from the Scientific Computing with Python Beta course. The bar graph was a bit of a challenge, but I really enjoyed this project.

I am looking for feedback on my code. Specifically, I’m curious if there built in functions that I should have used. I can tend to try to figure how to get things working “on my own” and sometimes miss things that would have made it easier to complete the project.

For example, for the Arithmetic Formulator, I forgot that split() existed and so built a split function from scratch. It worked, but over 20 lines of code was replaced by 2 once I remembered that I had that tool in my toolbox.

As far as my knowledge base goes, I started on Python with this curriculum, so I should know everything to this point in the curriculum, but do not bring any extra Python knowledge to the table.
Thank you for taking the time to review my project!

#Creates the budget category class and class methods
class Category:
    def __init__(self, category):
        self.category = category
        self.amount = 0
        self.description = ""
        self.ledger = []

    #When printing the category, prints the list of category transactions.
    def __str__(self):
        return f'{self.print_category(self.category)}'

    #Adds ledger entry for deposit with amount and optional description
    def deposit(self, amount, description = ""):
        self.ledger.append({'amount': amount, 'description': description})
    #Adds ledger entry for withdrawal with amount and optional description
    def withdraw(self, amount, description = ""):
        if self.check_funds(amount):
            amount = amount * -1
            self.ledger.append({'amount': amount, 'description': description})
            return True
        else:
            return False     
    #Returns current balance of category.
    def get_balance(self):
        bal = 0
        i = 0
        for item in self.ledger:
            amount = self.ledger[i]["amount"]
            bal = bal + amount
            i += 1
        return bal
    #Adds ledger entry for each category with deposit/withdrawal amount and description of where the transfer came from/to
    def transfer(self, amount, other_category):
        if self.check_funds(amount):
            description = 'Transfer to ' + other_category.category
            self.withdraw(amount, description)
            description = 'Transfer from ' + self.category
            other_category.deposit(amount, description)
            return True
        else: return False
    # Accepts an amount as an argument to see if there are sufficient funds for a transaction
    def check_funds(self, amount):
        if amount > self.get_balance():
            return False
        else:
            return True
    #creates a list of the transactions in the category beneath the category name with asterisks filling up to 30 total characters and the category's current balance
    def print_category(self, name):
        stars = (30 - len(self.category)) // 2
        star_line = ""
        title = ""
        i = 0
        while i < stars:
            star_line = star_line + "*"
            i += 1
        if len(f'{star_line}{name}{star_line}') != 30:
            title = f'{star_line}{name}{star_line}*'

        else:
            title = f'{star_line}{name}{star_line}'
        ret_string = ""
        for index in range(len(self.ledger)):
            size = len(f'{self.ledger[index]["description"][:23]}{self.ledger[index]["amount"]:.2f}')
            spaces = ""
            x = 0
            while x < 30 - size:
                spaces = spaces + " "
                x += 1
            ret_string = ret_string + f'{self.ledger[index]["description"][:23]}{spaces}{self.ledger[index]["amount"]:.2f}\n'   
        total = f'Total: {self.get_balance():.2f}'
        return f'{title}\n{ret_string}{total}'

#Creates a chart to show the distribution of spending as a percentage of total spent
def create_spend_chart(categories):
    total = _get_total(categories)
    cat_totals = []
    sumline= _make_sum_line(categories)
    i = 0
    #creates a tuple of the category name and a string of os with each o representing 10% of total spending rounded down to the nearest 10%.
    while i < len(categories):
        cat_totals.append((categories[i].category, _make_circles(round(_get_cat_total(categories[i])/total,2))))
        i += 1
    names = _make_category_names(categories)
    sa = []
    ss = " "
    j = 0
    #iterates through each category string of " " and "o" stored in cat_totals to place create an array (sa) of strings (ss). Each item of the category strings are separated by 2 spaces. These are then used to return the graph line by line in the return statement, so sa[0] would return the first ss string containing either " " or "o" for the category followed by two spaces between each.
    while j <= 10:
        for item in cat_totals:
            ss = ss + item[1][j] + "  "
        sa.append(ss)
        ss = " "
        j += 1
    #Returns chart in required format
    return(f'Percentage spent by category\n100|{sa[0]}\n 90|{sa[1]}\n 80|{sa[2]}\n 70|{sa[3]}\n 60|{sa[4]}\n 50|{sa[5]}\n 40|{sa[6]}\n 30|{sa[7]}\n 20|{sa[8]}\n 10|{sa[9]}\n  0|{sa[10]}\n{sumline}\n{names}')

#Generates a sum of all the spending transactions in a category
def _get_cat_total(category):
    spending_sum = 0
    i = 0
    while i < len(category.ledger):
        if category.ledger[i]['amount'] < 0:
            spending_sum = spending_sum + round(category.ledger[i]['amount'],2)
            i += 1
        else:
            i +=1
    return round(spending_sum,2)

#Generates a sting of " " and "o". Each circle represents 10% or more in spending for a category against the total spending. All categories will have a "o" for 0%, to the string is 11 characters long.
def _make_circles(percent):
    circle_number = percent * 10
    circle = ""
    i = 10
    while i >= 0:
        if i > circle_number:
            circle = circle + " "
        else:
            circle = circle + "o"
        i -= 1
    return circle

#Generates a sum of all spending in all categories to be used to determin percentage of spending in each category
def _get_total(categories):
    total = 0
    i = 0
    while i < len(categories):
        total = total + _get_cat_total(categories[i])
        i += 1
    return round(total,2)

#Generates the X-axis for the spending chart that extends two spaces past the final bar
def _make_sum_line(categories):
    sumline = "    -"
    i = 1
    while i <= len(categories):
        sumline = sumline + "---"
        i += 1
    sumline = sumline + ""
    return sumline

#Similar to the impletmentation of the circles above. Iterates over each name and adds the letter at the current index to a string. The string will print the category names vertically with two spaces between each letter. The string does not have an enter at the end.
def _make_category_names(categories):
    name_list = []
    for name in categories:
        name_list.append(name.category)
    max_length = (len(max(name_list, key = len)))
    test = "     "
    out = []
    i = 0
    while i < max_length:
        for name in name_list:
            if i >= len(name):
                test = test + "   "
            else:
                test = test + name[i] + "  "
        out.append(test)
        i += 1
        test = "     "
    return_string = ""
    for num, index in enumerate(out):
        if num == len(out)-1:
            return_string = return_string + index
        else:
            return_string = return_string + index + "\n"
    return return_string

First two things that jump out at me:

Here, I would just directly return the result of a Boolean comparison

Variable names should be words that people can read

1 Like

Perfect. Thank you. I do need to change sa and ss. I think that was in the midst of trying to go back and try a few things and used shorthand so I didn’t have to change as much, but forgot to go back and rename them. I like the idea of the Boolean as well. Thanks for taking the time!

Another thing sticking out is high usage of while loop. Usually they can be easily replaced with the for loop. In various variants of iterating over something for loop out of the box has advantage of skipping all manual setup that can contain mistakes - getting correct condition, incrementing, etc.
For example, this while loop:

sumline = "    -"
i = 1
while i <= len(categories):
    sumline = sumline + "---"
    i += 1

Is a for loop in disquise:

sumline = "    -"
for _ in categories:
    sumline = sumline + '---'

There are cases of using for loop, but just like it would be the while loop, for example:

def get_balance(self):
    bal = 0
    i = 0
    for item in self.ledger:
        amount = self.ledger[i]["amount"]
        bal = bal + amount
        i += 1
    return bal

item variable is not used at all, while item actually will be the object that in here is referred to as self.ledger[i]


Regarding built-in functions, I see you are using at some points range and enumerate. Both these functions do great work for making for loops more readable and helping with replacing while loops. They aren’t though utilized everywhere where it could in code. Taking another look at the code, after everything works, to simplify and refactor, would be beneficial.

In some situations loop can be omitted all together, if it’s for example just summing sequence of numbers, as there’s built-in sum function.


Take also a look at the str type itself. For example str can be multiplied by number, what will result in repeating the initial text specified number of times. Other string feature is formatting, particularly in this project, padding to specified number of characters can be useful.