3

I have a simple python (version 2.7.3) code that has an output that I can't figure out. The code prompts the user for a score (and will continue to do so if the input is anything other than a number from 0 to 1), determines the letter grade, and then exits. The code is as follows:

def calc_grade():
    try:
        score = float(raw_input("Enter a score: "))
        if score > 1.0:
            print "Error: Score cannot be greater than 1."
            calc_grade()
    except:
        print "Error: Score must be a numeric value from 0 to 1."
        calc_grade()

    print "\nthe score is: %s" % (score)
    if score >= 0.9:
        print "A"
    elif score >= 0.8:
        print "B"
    elif score >= 0.7:
        print "C"
    elif score >= 0.6:
        print "D"
    else:
        print "F"
    return 0
calc_grade()

If I run this script an try the inputs: 1.5, h, 0.8, then I get the following output:

Enter a score: 1.5
Error: Score cannot be greater than 1.
Enter a score: h
Error: Score must be a numeric value from 0 to 1.
Enter a score: 0.8

the score is: 0.8
B
Error: Score must be a numeric value from 0 to 1.
Enter a score: 0.7

the score is: 0.7
C

the score is: 1.5
A

As you can see, after entering a valid value (0.8), the script prints out the correct grade (B), but then script doesn't end as I expect it to. Instead, it prints out the error message for a non-numeric value, and then prompts the user to enter a score again. If I enter another valid score (0.7 in this case), then script prints out the correct grade (C), and then prints out the first incorrect input (1.5) along with its grade (A).

I can't, for the life of me, figure out what's causing this, "functionality". Any suggestions?

1
  • Your exception handler is catching an UnboundLocalError in addition to the ValueError that you are expecting. Commented Jun 24, 2015 at 22:35

5 Answers 5

7

On any error that occurs, you call calc_grade recursively again, so if you entered an invalid input, you'd have several calls. Instead, you should handle faulty errors iteratively:

def calc_grade():
    score = None
    while score is None:     
        try:
            score = float(raw_input("Enter a score: "))
            if score > 1.0:
                print "Error: Score cannot be greater than 1."
                score = None
        except:
            print "Error: Score must be a numeric value from 0 to 1."

    # If we reached here, score is valid,
    # continue with the rest of the code
Sign up to request clarification or add additional context in comments.

1 Comment

Ah, good point. So, if I understand this correctly, after the first incorrect value I'll have calc_grade() called prior to the remainder of the first call of calc_grade() finishing? If that's the case, then what cases the second non-numeric message to print? It looks like I could resolve this issue with a return after the recursive calling of calc_grade(); does this method violate any best-practices for code development?
1

Here's what happened:

When you passed your function the value "h", the casting of "h" to float failed, which threw a ValueError. Your except statement caught the error, and then called calcGrade() again. This new call was given an argument of .8, and returned normally. When the .8 call returned, it returned control back to the call the had received "h" as an argument. That call then proceeded to execute its next instruction: print "\nthe score is: %s" % (score). Since the cast to float had failed, score was never assigned. Therefore, that call to calcGrade() throws an UnboundLocalError, which is then caught by its caller, which is the instance of calcGrade() that was passed the value 1.5 (as @ZackTanner pointed out). Recall that the "h" call was called from inside the try block.

Comments

0

Recusion is biting you because you have additional code in your function after the recursion. @Mureink 's answer is a valid way to handle this. Another is to make the data input action it's own function:

def get_input():
    try:
        score = float(raw_input("Enter a score: "))
        if score > 1.0:
            print "Error: Score cannot be greater than 1."
            return get_input()
    except ValueError:
        print "Error: Score must be a numeric value from 0 to 1."
        return get_input()
    return score

def calc_grade():
    score = get_input()
    print "\nthe score is: %s" % (score)
    if score >= 0.9:
        print "A"
    elif score >= 0.8:
        print "B"
    elif score >= 0.7:
        print "C"
    elif score >= 0.6:
        print "D"
    else:
        print "F"
    return 0
calc_grade()

This technique returns the entered value when the user enters a valid value. When they do not, it returns the value of calling get_input(). this stacks up all the recursions ready to return whatever gets returned to them. When the user finally enters a valid response, the entire recursion stack collapses returning the valid answer that the user entered.

The call to get_input() within calc_grade will process until the user enters a valid answer. At that time get_input will cease processing and return that valid user input to calc_grade for calc_grade to do its processing.

2 Comments

Thanks, Alan. I believe I have a partial understanding of how the recursion is biting me, but there's still a detail I'm missing (see my response to @Mureink).
@Shaun I edited to add more explanation. Hope this helps.
0

You are forgetting that recursion doesn't terminate the previous call to the function. So when you call calc_grade() at the error you then return to the original calc_grade() to print "the score is:" and that's why it prints several times.

Now, to fix your code, I'd just add some returs:

def calc_grade():
    try:
        score = float(raw_input("Enter a score: "))
        if score > 1.0:
            print "Error: Score cannot be greater than 1."
            calc_grade()
            return
    except:
        print "Error: Score must be a numeric value from 0 to 1."
        calc_grade()
        return    
    print "\nthe score is: %s" % (score)
    if score >= 0.9:
        print "A"
    elif score >= 0.8:
        print "B"
    elif score >= 0.7:
        print "C"
    elif score >= 0.6:
        print "D"
    else:
        print "F"
calc_grade()

Python doesn't require you to write anything after return, you can use it to simply exit the function.

Also I recommend using str.format as oposed to the % formatting.

This is how I'd do it without modifying your code too much:

def calc_grade():
    try:
        score = float(raw_input("Enter a score: "))
        if score > 1.0:
            raise TypeError
    except ValueError:
        print "Error: Score must be a numeric value from 0 to 1."
    except TypeError:
        print "Error: Score cannot be greater than 1."
    except:
        print "Error: Unexpected error, try again."
    else:
        if score >= 0.9:
            score = "A"
        elif score >= 0.8:
            score = "B"
        elif score >= 0.7:
            score = "C"
        elif score >= 0.6:
            score = "D"
        else:
            score = "F"
        print "the score is: {}".format(score)
        return
    calc_grade()

calc_grade()

Comments

0

First, you cannot call inside calc_grade(). That will run a bunch of errors. You can only call it once, but you can print it as many times as you want. Second, try and except might not be the best way to do it. Try making a class and making functions from there. try and except will be run every time your code finished running. Third, if you run a number between any of those numbers it will print all the letter before the maximum. I have code that is similar to yours, it calculates 3 peoples scores. Here is a website to help you better understand Errors and Exceptions. https://docs.python.org/2/tutorial/errors.html

Here is my code
    lloyd = {
        "name": "Lloyd",
        "homework": [90.0, 97.0, 75.0, 92.0],
        "quizzes": [88.0, 40.0, 94.0],
        "tests": [75.0, 90.0]
    }
    alice = {
        "name": "Alice",
        "homework": [100.0, 92.0, 98.0, 100.0],
        "quizzes": [82.0, 83.0, 91.0],
        "tests": [89.0, 97.0]
    }
    tyler = {
        "name": "Tyler",
        "homework": [0.0, 87.0, 75.0, 22.0],
        "quizzes": [0.0, 75.0, 78.0],
        "tests": [100.0, 100.0]
    }

# Add your function below! def average(numbers): total = sum(numbers) total = float(total) return total/len(numbers) def get_average(student): homework_ave=average(student["homework"]) quizzes_ave=average(student["quizzes"]) tests_ave=average(student["tests"]) return 0.1 * average(student["homework"]) + 0.3 * average(student["quizzes"]) + 0.6 * average(student["tests"]) def get_letter_grade(score): if 90 <= score: return "A" elif 80 <= score: return "B" elif 70 <= score: return "C" elif 60 <= score: return "D" else: return "F" print get_letter_grade(get_average(lloyd)) def get_class_average(students): results = [] for student in students: results.append(get_average(student)) return average(results) students = [lloyd, alice, tyler] print get_class_average(students) print get_letter_grade(get_class_average(students))

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.