7
\$\begingroup\$

I just started programming a week ago. Online tutorials aren't my thing, so I decided to just start projects and learn what I need for the project.

My first project is a guess the number game. The user is asked to guess a number between 0 and 20. If the user's guess is too high, they will be told it's high and likewise if it's too low. This will go on until the user guesses correctly.


print("Enter a random number between 0 and 20")
userInput = int(input())
random_number= random.randint(0,20)

def random_num_checker(num,guess):
    if num== guess:
        print('CORRECT')
    else:
        while guess != num:
            if guess > 20:
                print('I said between 0 and 20')
            if guess < num:
                print('too small')
            elif guess > num:
                print('too big')
            guess=int(input())
        print('CORRECT')
random_num_checker(random_number,userInput)   
\$\endgroup\$
6
\$\begingroup\$

Aside from the notes already given, here are two big ideas to think about:

Don't repeat yourself (DRY)

This is something you'll hear repeated a lot in discussions of code. Any time you see the same "magic values" repeated more than one place (e.g. 0 and 20), or you see two lines of code that do exactly the same thing for exactly the same reason (e.g. your input() statements or the multiple CORRECT checks) it's a clue that you have an opportunity to share some code.

In the case of the magic numbers, one solution is to define them as constants at the top of the file. The approach I'd prefer, personally, would be to make them parameters -- rather than hardcoding 0 and 20, have your function(s) take a range as a parameter.

In the case of the multiple "CORRECT" checks, this is just a matter of restructuring the loop a little.

Have each function do one thing well

This is significantly more subjective, but in looking at your function, I see two very separable things it's doing within its single main loop -- prompting the user for valid input, and giving them feedback on their guess. If you have one piece of code that just has to get the input without having to worry about the game loop, and you have another that handles the game loop without having to worry about whether the input is valid, then both of them can be simpler.

When you start doing larger projects, breaking logic apart into simple functions makes it easier to test the individual units of your code independently.

I messed around with the code for a bit using those two principles and came up with this:

import random

def describe_range(numbers: range) -> str:
    """Nice English-language description of a range."""
    return f"between {numbers[0]} and {numbers[-1]}"

def input_number(numbers: range) -> int:
    """
    Prompt the user for a number within the range,
    retrying on invalid input.
    """
    while True:
        try:
            number = int(input())
            if not number in numbers:
                raise ValueError(f"The number needs to be {describe_range(numbers)}")
            return number
        except ValueError as e:
            print(e)

def guess_number(numbers: range) -> None:
    """
    Play a guessing game with the user within a range of numbers.
    Tell them whether their guess is too big or too small.  
    They win when they get it right!
    """
    print("Enter a random number", describe_range(numbers))
    answer = random.choice(numbers)
    while True:
        guess = input_number(numbers)
        if guess > answer:
            print("too big")
        elif guess < answer:
            print("too small")
        else:
            print("CORRECT")
            break

if __name__ == "__main__":
    guess_number(range(21))

Note that I'm using Python 3's type annotations to say what type of argument each function takes and what it returns -- this is really handy because you can use the mypy tool to automatically check your code for errors (for example if you say a function returns an int and there's a line in that function that returns a str, mypy will throw an error), and therefore you don't need to worry as much about your code getting the wrong type at runtime. The type annotations also serve as documentation for your functions so that human readers can easily see what sorts of values they should expect the code to be working with.

| improve this answer | |
\$\endgroup\$
4
\$\begingroup\$

First off, some minor nitpicks on style.

  • In Python, variables are usually given names like_this rather than likeThis - most of your names are fine, but userInput should probably be user_input instead.
  • You usually want a space on each side of operators, guess = int(input()) is more pleasant to look at than guess=int(input())

Second, your program's behaviour is different from what I'd expect in some subtle ways.

  • If I enter a number above 20, I get both a reminder that "[you] said between 0 and 20" and a message that says my guess is "too big". While both are accurate, only the first is really necessary.
  • If I enter a negative number, I'm not reminded that my guess must be between 0 and 20, I'm just told that my guess is too low. Again, that is true, but since you already have a message that says my guess is outside the acceptable range, I'd expect to see that message instead.
  • If I enter something that isn't a number at all, the program crashes entirely. I would've expected you to catch the ValueError thrown by the guess = int(input()) line and give me a cheeky error message instead.

Third, structure.

  • Why is the if num == guess there? The program seems to behave exactly the same if that function begins with the while ... line, and the code feels a bit cleaner.
  • random_num_checker is reponsible for asking the player for numbers until they guess the right one. Why, then, does it not also prompt for the first guess, but rather expect that one to be passed as an argument? It feels weird.
  • Constants should preferably be defined in one place only, in case you want to change them. I'd have liked to see the lowest and highest possible guesses be defined as constants. It's not a huge deal in a small program like this, but it's generally good practice.
  • On a related note, taking the minimumand maximum allowed guesses as parameters would make the random_num_checker function more flexible.

Based on this, I'd write something kind of like this:

MIN_GUESS = 0
MAX_GUESS = 20

def random_num_checker(goal, min_guess, max_guess):
    # Some sanity checks, in case someone wants to use this function for their own game
    if type(goal) is not int:
        raise TypeError(f'Goal must be an int, was {type(goal)}')
    elif goal < min_guess or goal > max_guess:
        raise ValueError(f'Goal must be within the allowed guessing range ({min_guess}-{max_guess}), was {goal}')

    print(f'Enter a number between {min_guess} and {max_guess}')
    guess = None
    while guess != goal:
        try:
            guess = int(input())

            if guess > max_guess or guess < min_guess:
                print(f'I said between {min_guess} and {max_guess}')
            elif guess > goal:
                print('Too high')
            elif guess < goal:
                print('Too low')
        except ValueError:
            print('That doesn\'t look like a number to me')
            guess = None

    print("CORRECT")


random_number = random.randint(MIN_GUESS, MAX_GUESS)
random_num_checker(random_number, MIN_GUESS, MAX_GUESS)
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Is it necessary to check if "goal" is an int since it's an int generated by Python? \$\endgroup\$ – pablowolf96 Mar 28 at 20:01
  • \$\begingroup\$ Sara explained in a comment that the purpose of this check was so that the function could potentially be used elsewhere. You know you aren’t going to feed it a string today, but you might do it accidentally later. Or somebody else might. It’s just insurance. \$\endgroup\$ – Preston Mar 30 at 6:31
3
\$\begingroup\$

To add to @Sara J's answer, in Python, it's generally a good practice to wrap your main code (so the last two lines) in a if __name__ == '__main__': statement so your script can be either:

  • Directly run
  • Imported and its functions used as the dev that imported it pleases.

https://stackoverflow.com/a/419185/1524913

Also, contrary to a lot of other programming languages, Python insist on the if it quacks, it's a duck way of handling things:

Usually, in Python, when possible, you don't check data first. You just run the code and wrap it in a try ... except block. If it works (quack) you don't need to type-check, etc. So, eg, you'd do:

try:
    userInput = int(input(f"Enter a number between {MIN_NUMBER} and {MAX_NUMBER}: ")
except ValueError:
    print("Please enter a valid number!")

So, all in all:

MIN_GUESS = 0
MAX_GUESS = 20

def random_num_checker(goal, min_guess, max_guess):
    if goal < min_guess or goal > max_guess:
        raise ValueError("Goal is outside the min/max bounds")

    print(f'Enter a number between {min_guess} and {max_guess}')

    guess = None
    while guess != goal:
        try:
            # Could also (re-)ask every time instead of only once at the beginning
            guess = int(input())
        except ValueError: # Only try to catch what's needed
            print('That doesn\'t look like a number to me')
            continue

        if min_guess < guess < max_guess:
            if guess > goal:
               print('Too high')
            elif guess < goal:
               print('Too low')
        else:
            print(f'I said between {min_guess} and {max_guess}')

    print("CORRECT")

if __name__ == '__main__':
   goal = random.randint(MIN_GUESS, MAX_GUESS)
   random_num_checker(goal, MIN_GUESS, MAX_GUESS)
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Personally, I'd expect calls like random_num_checker(0.5, 0, 1) to throw an exception, since the user will never be able to successfully guess anything but a whole number since their input will be converted to an int. But I agree that checking if type(goal) is int isn't very pythonic, and checking if goal % 1 == 0 might be better here \$\endgroup\$ – Sara J Mar 28 at 1:34
  • \$\begingroup\$ @SaraJ oooh that was on goal... gotchu. I'm not sure what would be the most pythonic way to do that then. 🤔 \$\endgroup\$ – jeromej Mar 28 at 16:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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