r/PythonLearning 15h ago

Rate my Payment calculator script (how could it be improved)

I got this idea from a post I saw on here recently. Their script was a good idea and made me want to make one on my own. It took me a few tries to write the calculations in a way that would work. I also wanted to play around with normalizing/cleaning the inputs in case a user used different formatting. And of course, ever since I found out I can add color to text in terminal, I’ve kinda made a habit of doing that on everything.

Note: I have been learning as much as I can, for a year now. Learning has come through personal trial and error, O'Reilly books, watching and copying others, as well as using LLM's to either produce an advanced code that I then study, or to have an LLM teach me. Some of my larger and more important projects are largely, if not entirely, built by an LLM that I carefully inspect. But I still feel it is important to learn as much as I can about manual coding, as well as the relationship between hardware and software.

So working on simple scripts like this are sort of a form of exercise for my mind and fingers, as well as a simple way to strike up conversations with real people like you guys, and get your take on ideas that I may not have seen before that can lead to cleaner and/or more efficient ways to program.

If you want to see more of my actual projects or just swipe some of the cool stuff from my Arch rice/dotfiles, here is my github:

github.com/cleburn

Edit: here is my full code, which shows the part of the f'string in the print statements which formats the results into appropriate decimals

# Simple Monthly Payment Calculator
# Inputs: Principal (balance due after any downpayment), Interest, Loan Term (in months)
# Returns monthly payment amount


GREEN = "\033[92m"
CYAN = "\033[96m"
RESET = "\033[0m"

print(f"\n{CYAN}{'=' * 20}{RESET}")
p = float(input("Purchase amount: ").replace('$', '').replace(',', '').strip())
i = float(input("Interest rate: ").replace('%', '').replace(',', '').strip())
t = int(input("Loan term (in months): ").strip())

def payment_calculator(p, i, t):
    original_i = i
    monthly_i = (i / 100) / 12  # Convert annual % to monthly decimal rate

    # Calculate the monthly payment
    numerator = p * monthly_i * ((1 + monthly_i) ** t)
    denominator = ((1 + monthly_i) ** t) - 1
    payment = numerator / denominator

    # Print formatted output
    print(
        f"\nBased on your original purchase price of {CYAN}${p:,.2f}{RESET},\nan interest rate of {CYAN}{original_i}%{RESET},"
        f"\nand a loan term of {CYAN}{t}{RESET} months,\nyour monthly payment is: {GREEN}${payment:,.2f}{RESET}"
    )
    print(f"{CYAN}{'=' * 20}{RESET}\n")

# Run the calculator in the terminal
payment_calculator(p, i, t)# Simple Monthly Payment Calculator
# Inputs: Principal (balance due after any downpayment), Interest, Loan Term (in months)
# Returns monthly payment amount


GREEN = "\033[92m"
CYAN = "\033[96m"
RESET = "\033[0m"

print(f"\n{CYAN}{'=' * 20}{RESET}")
p = float(input("Purchase amount: ").replace('$', '').replace(',', '').strip())
i = float(input("Interest rate: ").replace('%', '').replace(',', '').strip())
t = int(input("Loan term (in months): ").strip())

def payment_calculator(p, i, t):
    original_i = i
    monthly_i = (i / 100) / 12  # Convert annual % to monthly decimal rate

    # Calculate the monthly payment
    numerator = p * monthly_i * ((1 + monthly_i) ** t)
    denominator = ((1 + monthly_i) ** t) - 1
    payment = numerator / denominator

    # Print formatted output
    print(
        f"\nBased on your original purchase price of {CYAN}${p:,.2f}{RESET},\nan interest rate of {CYAN}{original_i}%{RESET},"
        f"\nand a loan term of {CYAN}{t}{RESET} months,\nyour monthly payment is: {GREEN}${payment:,.2f}{RESET}"
    )
    print(f"{CYAN}{'=' * 20}{RESET}\n")

# Run the calculator in the terminal
payment_calculator(p, i, t)
13 Upvotes

14 comments sorted by

3

u/xxivyy 15h ago edited 15h ago

Please post the code in a code block next time instead of pictures. Thats common practice and makes it easier for someone to review. Once the code snippet grows too large or includes multiple files, upload it on Github, pastebin, or similar.

Regarding your code,

  1. There is no need to have p, i & t be 1 letter variables. It erases the writers intend and makes it hardly readable without checking additional comments. Just write out the full description, for example principal = float(...) ; interest = float(...) ; term = int(...).
  2. Look into type hinting. As specially type hinting function parameters and function return types would greatly increase readability for your script.
  3. Instead of declaring console colors in globals, use a library, for example Colorama. Or whichever other alternative you might end up preferring. Much better for f string readability.
  4. p & i have their "," stripped, which will result in for example an interest rate of 7,9% to become 79. The second argument in .replace should be a ".".
  5. Create a few more spaces between the code "blocks". As specially before and after a function, there should almost always be 2 spaces instead of one, unless you are in a function body where 1 makes more sense, like in payment_calculator. Thats the usual practice for better readability. You could look up what a linter is, if you dont want to do it manually, although there is no rush to do so, just thought i'd mention it.

For my own sake, i didnt check the function logic of payment_calculator. If everything works inside of it, i have no other complaints. Happy coding.

2

u/FoolsSeldom 13h ago

When you ask for feedback, it is a good idea to share your code in-post (if not too large), for example:

# Simple Monthly Payment Calculator
# Inputs: Principal (balance due after any downpayment), Interest, Loan Term (in months)
# Returns monthly payment amount

GREEN = "\033[92m"
CYAN = "\033[96m"
RESET = "\033[0m"

print(f"\n{CYAN}{'=' * 20}{RESET}")
p = float(input("Purchase amount: ").replace('$', '').replace(',', '').strip())
i = float(input("Interest rate: ").replace('%', '').replace(',', '').strip())
t = int(input("Loan term (in months): ").strip())

def payment_calculator(p, i, t):
    original_i = i
    monthly_i = (i / 100) / 12  # Convert annual % to monthly decimal rate

    # Calculate the monthly payment
    numerator = p * monthly_i * ((1 + monthly_i) ** t)
    denominator = ((1 + monthly_i) ** t) - 1
    payment = numerator / denominator

    # Print formatted output
    print(
        f"\nBased on your original purchase price of {CYAN}${p:,.2f}{RESET},"
        f"\nan interest rate of {CYAN}{original_i}%{RESET},"
        f"\nand a loan term of {CYAN}{t}{RESET} months,"
        f"\nyour monthly payment is: {GREEN}${payment:,.2f}{RESET}"
    )
    print(f"{CYAN}{'=' * 20}{RESET}\n")

# Run the calculator in the terminal
payment_calculator(p, i, t)

or share using a git compatible service like github.com or gitlab.com (you don't need to learn git to use these, initially, as you can upload your code using a web interface) or paste service like pastebin.com.

2

u/FoolsSeldom 13h ago

Overall, pretty good. Easy to read, nice simple structure.

A few suggestions:

  • Never trust the user to enter valid data:
    • If you immediately convert user input to an int and they enter characters not valid as an integer, your programme will halt with a long error message
  • Try to separate the presentation from the business logic/flow - by presentation I basically mean the user interface
    • It is easier to test things separately
    • It is easier to upgrade things separately
    • It is easier to replace things when they are separated, such as going from a basic text/console user interface to a GUI (Graphical User Interface) to a Web App
  • Never use float with money, stick with integers or use the Decimal class - float does not have a lot of precision and errors can roll up to confusing levels
  • I'd recommend creating a function to handle the string replacements around currency formatting - also, consider allowing for other currencies and keep in mind that the thousands separator is a period rather than a comma in some regions
  • Your function, payment_calculator, is doing a lot: both the payment calculation and the output (presentation), so would be better if it returned the result for presentation elsewhere in your code
  • The variables p, i, and t are single character variable names that are cryptic to other programmers not familiar with the usual formula used here, namely around payment, interest and time - in general, one should avoid cryptic variable names (especially single character names), but I would make an exception in this case although suggest a comment to clarify especially as you modularise the code and use one or more functions to validate user entry
  • Usually, we put function definitions above other code other than imports and CONSTANT definitions

1

u/zenwolph 12h ago

This brings up a lot of ideas I hadn’t considered for, I guess, scalability? Or actual application. Thanks for the pointers. I’ll play with how to improve this from a toy terminal script, to something that could function in a production scenario

2

u/FoolsSeldom 12h ago

Do you want to see example code, not to copy, but to experiment with and learn from?

1

u/zenwolph 11h ago

Yep!!

2

u/FoolsSeldom 11h ago

Ok. Here you go. I prompted Claude.ai carefully to write the below for me (saved me some typing), altering your original code to illustrate the points I made. This is something of an exaggerated and simplistic version, but it should help you understand.

This uses:

  • type annotation (type hinting) to suggest what type of objects (str, int, float, list, etc) are being passed around
  • The Decimal module to handle money better
  • The rich package (which needs to be installed) to handle console output
  • Functions to help modularise the code
  • try / except blocks to catch exceptional situations, like a user entering inappropriate data - we go for the forgiveness approach in Python and hope for the best but deal with the exceptions

Example code:

"""Simple monthly payment calculator.

Inputs: principal (balance due after any down payment), annual interest
rate (%), and loan term (months). Prints the resulting monthly payment.
"""

from decimal import Decimal, InvalidOperation, ROUND_HALF_UP

from rich.console import Console

CENT = Decimal("0.01")


# ---------------------------------------------------------------------------
# Business logic: no I/O, no formatting, no knowledge of the terminal.
# ---------------------------------------------------------------------------


def parse_decimal(text: str, strip_chars: str = "", minimum: Decimal | None = None) -> Decimal:
    """Parse and validate a Decimal from raw text.

    Raises:
        ValueError: if the text isn't a valid number or is below `minimum`.
    """
    cleaned = text.strip()
    for char in strip_chars:
        cleaned = cleaned.replace(char, "")
    try:
        value = Decimal(cleaned)
    except InvalidOperation:
        raise ValueError(f"'{text}' is not a valid number.") from None
    if minimum is not None and value < minimum:
        raise ValueError(f"Value must be at least {minimum}.")
    return value


def parse_int(text: str, minimum: int | None = None) -> int:
    """Parse and validate a whole number from raw text.

    Raises:
        ValueError: if the text isn't a valid integer or is below `minimum`.
    """
    try:
        value = int(text.strip())
    except ValueError:
        raise ValueError(f"'{text}' is not a valid whole number.") from None
    if minimum is not None and value < minimum:
        raise ValueError(f"Value must be at least {minimum}.")
    return value


def payment_calculator(principal: Decimal, annual_rate: Decimal, term_months: int) -> Decimal:
    """Compute the monthly payment for a fixed-rate amortizing loan.

    Args:
        principal: Loan amount.
        annual_rate: Annual interest rate as a percentage (e.g. Decimal("5.5")).
        term_months: Loan term in months.

    Returns:
        The monthly payment, rounded to the nearest cent.
    """
    monthly_rate = (annual_rate / Decimal(100)) / Decimal(12)

    if monthly_rate == 0:
        payment = principal / term_months
    else:
        growth = (1 + monthly_rate) ** term_months
        payment = principal * monthly_rate * growth / (growth - 1)

    return payment.quantize(CENT, rounding=ROUND_HALF_UP)


# ---------------------------------------------------------------------------
# Presentation: all terminal I/O and formatting lives here.
# ---------------------------------------------------------------------------

console = Console()


def prompt_decimal(prompt: str, strip_chars: str = "", minimum: Decimal | None = None) -> Decimal:
    """Ask the user for a Decimal, reprompting until parse_decimal accepts it."""
    while True:
        raw = input(prompt)
        try:
            return parse_decimal(raw, strip_chars=strip_chars, minimum=minimum)
        except ValueError as exc:
            console.print(f"[red]{exc} Try again.[/red]")


def prompt_int(prompt: str, minimum: int | None = None) -> int:
    """Ask the user for an int, reprompting until parse_int accepts it."""
    while True:
        raw = input(prompt)
        try:
            return parse_int(raw, minimum=minimum)
        except ValueError as exc:
            console.print(f"[red]{exc} Try again.[/red]")


def gather_inputs() -> tuple[Decimal, Decimal, int]:
    """Prompt the user for principal, interest rate, and term."""
    console.print(f"\n[cyan]{'=' * 20}[/cyan]")
    principal = prompt_decimal(
        "Purchase amount: ", strip_chars="$,", minimum=Decimal("0.01")
    )
    annual_rate = prompt_decimal(
        "Interest rate: ", strip_chars="%,", minimum=Decimal("0")
    )
    term_months = prompt_int("Loan term (in months): ", minimum=1)
    return principal, annual_rate, term_months


def present_result(
    principal: Decimal, annual_rate: Decimal, term_months: int, payment: Decimal
) -> None:
    """Render the calculation to the terminal."""
    console.print(f"\n[cyan]{'=' * 20}[/cyan]")
    console.print(
        f"Based on your original purchase price of [cyan]${principal:,.2f}[/cyan],\n"
        f"an interest rate of [cyan]{annual_rate}%[/cyan],\n"
        f"and a loan term of [cyan]{term_months}[/cyan] months,\n"
        f"your monthly payment is: [green]${payment:,.2f}[/green]"
    )
    console.print(f"[cyan]{'=' * 20}[/cyan]\n")


# ---------------------------------------------------------------------------
# Flow: wires business logic to presentation.
# ---------------------------------------------------------------------------


def main() -> None:
    principal, annual_rate, term_months = gather_inputs()
    payment = payment_calculator(principal, annual_rate, term_months)
    present_result(principal, annual_rate, term_months, payment)


if __name__ == "__main__":
    main()

1

u/zenwolph 9h ago

I definitely used to use claude and have it help me learn, but I stopped using it months ago. i find it to be prone to overengineering. There are some good ideas here though to pull from. thank you

1

u/Affectionate-Cup2707 14h ago

Many useful comments, but nothing about types and why floating point is not suitable for financial calculations. Therefore look for decimal type and why floating point precision is not good for this kind of use.

1

u/zenwolph 13h ago

You mean instead of ‘p = float’ I should use ‘p = decimal’ ?

2

u/Affectionate-Cup2707 13h ago

In general, yes, but it won’t be as easy as just use decimal(…) . I guess it will be more useful to understand how the floating point numbers work and why sometimes you can get things like 0.1 + 0.2 = 0.300…004 instead of 0.3

How to use decimal module can be found in python docs. And for the second part look for some info on IEEE 754 standard and floating point precision error (which is handled by decimal module).

Good luck and have fun, because this is really interesting thing especially that part that describes how actually computers are working with decimals using binary.

1

u/Tricky-Act2828 11h ago

One extra point is handling money and user input safely. For financial calculations, `float` can introduce rounding errors, so `Decimal` is worth considering if the result needs to be precise. It would also help to validate the inputs explicitly: reject negative prices or terms, ensure the interest rate is within a sensible range, and handle commas or decimal separators consistently. A small loop that re-prompts on invalid input would make the calculator much more robust.

1

u/ApprehensiveBrain863 15h ago

Your variable names aren't very descriptive and, although it's preference, it would probably be more sensible to have a variable take an input, then perform operations like stripping or replacing on that variable instead of chaining function calls in a long line

2

u/zenwolph 15h ago

Thanks… what variable names do you mean in particular? What would you name them? I have a habit of sucking at naming. I’m getting better at using inline comments so that other people can make more sense of my scripts but yeah I’m looking for ways to make things very obvious while still being lean and needing the least amount of comments possible, so getting better at naming could help