Struggling with combining while loop, if statement and try block

Hi there,
I’m new to python. I just finished eric matthes crash course book and working on my first script. Its a program that calculates income tax and I’m having an issue with one of my functions which accepts user input for which year they would like to calculate tax for.

I want it to give an error if user inputs a year outside the range or if they input an non integer value and then loop back and re-ask the question. If they enter a year in the correct range I want them to break the loop and return the year which I will use later in my script

I can handle writing the function for one of these scenarios (ie. erroring and looping back if the user enters a year outside the range) but when I try and combine them im getting really confused. I tried nesting an if block in a try block in a while loop but it wasnt working. In the end I broke it into two functions but not sure if I am over complicating things

def confirm_tax_year():
    while True:
        try:
            year = int(input("Which year are you calculating tax for? (Taxbuddy can calculate from 2020 to 2024) "))
        except ValueError:
            print("Invalid input try again")
        else:
            _year_in_range(year)
            break

def _year_in_range(year):
    if year in range(2020,2025):
        return year
    else:
        print("This year is not in the range 2020 to 2024 please try again")
        confirm_tax_year()

Make _year_in_range a predicate function (one that returns True or False), and only break the loop if it returns True. It would be nice to add a helpful “out of year range” error message if it returns False.

If you move the if / else out of the function into the loop, replacing return year with break, and removing confirm_tax_year(), there’s no longer any need for the function aside from modularity.

1 Like

I can’t see any issue here with your understanding of while, if or try.

The issue is your understanding of functions.

The most important thing to understand is that a function is not simply a label for a place where you can “jump” in the code. A function is a self-contained mini-program: it takes in some input from its parameters, and gives back a result via return.

So, we don’t want to call confirm_tax_year again from _year_in_range to “restart” the loop. Instead, we want confirm_tax_year to use the result to decide whether to break.

We use if to make a decision, as you understand well enough; and here we can simply nest that underneath the else: of your try block. That is: in the cases where there is no ValueError, we should next check if the _year_in_range.

In order to make that decision sensibly, _year_in_range needs to tell us whether the year is in range. So it should return a boolean value: True (the year is in range) or False (it isn’t).

With those changes, we get:

def confirm_tax_year():
    while True:
        try:
            year = int(input("Which year are you calculating tax for? (Taxbuddy can calculate from 2020 to 2024) "))
        except ValueError:
            print("Invalid input try again")
        else:
            if _year_in_range(year):
                break

def _year_in_range(year):
    if year in range(2020,2025):
        return True
    else:
        print("This year is not in the range 2020 to 2024 please try again")
        return False

But that said, with more understanding, we can structure the code more clearly.

At this point, you may think that separating out the function isn’t helping very much - it only does its own if check that we could use directly instead, and sometimes prints a message. Here’s what it looks like without separating out the function:

def confirm_tax_year():
    while True:
        try:
            year = int(input("Which year are you calculating tax for? (Taxbuddy can calculate from 2020 to 2024) "))
        except ValueError:
            print("Invalid input try again")
        else:
            if year in range(2020,2025):
                break
            else:
                print("This year is not in the range 2020 to 2024 please try again")

Notice there isn’t anything to return any more, because a) we won’t use that information to make a decision (we already made it) and b) now it would “return” from confirm_tax_year, which we don’t want to do.

There’s another trick we can do to simplify this. Instead of having an else for the try - if we make sure that the code is only reached when there is no exception, we can put the next part of the code right after, and save the else. To do that, we need to think about what happens when there is an exception. In this case, we don’t want to run the year-checking code, but we do want to ask for another input. The natural way to get that result is to continue the loop. So:

def confirm_tax_year():
    while True:
        try:
            year = int(input("Which year are you calculating tax for? (Taxbuddy can calculate from 2020 to 2024) "))
        except ValueError:
            print("Invalid input try again")
            continue
        if year in range(2020,2025):
            break
        else:
            print("This year is not in the range 2020 to 2024 please try again")

When it comes to this kind of “validation loop”, my preference is to turn it into a series of checks for each thing that can go wrong. Each one is tried, and when something is wrong, the loop continues. Then after everything is checked, we can break unconditionally:

def confirm_tax_year():
    while True:
        try:
            year = int(input("Which year are you calculating tax for? (Taxbuddy can calculate from 2020 to 2024) "))
        except ValueError:
            print("Invalid input try again")
            continue
        if year not in range(2020,2025):
            print("This year is not in the range 2020 to 2024 please try again")
            continue
        break

Let’s also separate the request for input (since that should always succeed, and just give us a string) from the checks:

def confirm_tax_year():
    while True:
        year = input("Which year are you calculating tax for? (Taxbuddy can calculate from 2020 to 2024) ")
        # make sure it's an integer
        try:
            year = int(year)
        except ValueError:
            print("Invalid input try again")
            continue
        # make sure it's in range
        if year not in range(2020,2025):
            print("This year is not in the range 2020 to 2024 please try again")
            continue
        break

At this point, we can experiment with functions again. It should be easy enough to start using the fixed _year_in_range again, if you wanted. Maybe you might also imagine a _year_is_integer function. There’s a problem here: you would need to know whether the int call succeeded, but you also need to get the actual integer result. So you might for example return a tuple of those two values (you’ll have to make something up for the “integer result” when the call failed - just to have a consistently structured result; it doesn’t really matter, since the main loop won’t use that value).

Wow thanks so much for the detailed reply. I think I was initially trying to do something like what you did in the second example where the one function comprised both checks but I couldnt get it right.

The continue isnt something I used before but it looks very useful. If I need the function to return the year value for a seperate part of my script can I add a return like below before the ultimate break?

def confirm_tax_year():
    while True:
        year = input("Which year are you calculating tax for? (Taxbuddy can calculate from 2020 to 2024) ")
        # make sure it's an integer
        try:
            year = int(year)
        except ValueError:
            print("Invalid input try again")
            continue
        # make sure it's in range
        if year not in range(2020,2025):
            print("This year is not in the range 2020 to 2024 please try again")
            continue
        return year
        break

ah thats an interesting idea. Thanks for the suggestion. Im just trying to use this script to practise as much as what I’ve learned as possible so will give this a go too

There’s no point in putting a break after a return because it’ll never get there!

2 Likes

Yes. Keep in mind that return ends the current call to the function immediately (once you’ve said “we’re done and this is the result”, it’s not possible to do more work), so the break in this example is useless (won’t ever be reached).

1 Like

ah okay, I didnt know that, does return serve as a break also then (in addition to returning a value obviously)

In a function or generator, yes.

1 Like

Here is another excellent book that is comprehensive and covers almost all Python fundamentals:

Learning Python, 5th Edition by Mark Lutz

It is not gimmicky. It soundly covers the material thoroughly with many examples.

1 Like

Al Sweigart’s book The Big Book of Small Python Projects has lots of fun little projects you can do with examples and explainers. And it’s only $1.99 for the ebook.

1 Like