12
\$\begingroup\$

On my quest to become a master programmer I have created a simple game of Hangman, and so I thought I would upload it here and see how it could be refined.

One question I had was my use of exception handling. I tried to utilise it here, but I get the sense it might have been overkill. Should I only be using try/except if I know there will be exceptions, or is it good practice to use them regardless?

"""Hangman
Standard game of Hangman. A word is chosen at random from a list and the
user must guess the word letter by letter before running out of attempts."""

import random

def main():
    welcome = ['Welcome to Hangman! A word will be chosen at random and',
               'you must try to guess the word correctly letter by letter',
               'before you run out of attempts. Good luck!'
               ]

    for line in welcome:
        print(line, sep='\n')

    # setting up the play_again loop

    play_again = True

    while play_again:
        # set up the game loop

        words = ["hangman", "chairs", "backpack", "bodywash", "clothing",
                 "computer", "python", "program", "glasses", "sweatshirt",
                 "sweatpants", "mattress", "friends", "clocks", "biology",
                 "algebra", "suitcase", "knives", "ninjas", "shampoo"
                 ]

        chosen_word = random.choice(words).lower()
        player_guess = None # will hold the players guess
        guessed_letters = [] # a list of letters guessed so far
        word_guessed = []
        for letter in chosen_word:
            word_guessed.append("-") # create an unguessed, blank version of the word
        joined_word = None # joins the words in the list word_guessed

        HANGMAN = (
"""
-----
|   |
|
|
|
|
|
|
|
--------
""",
"""
-----
|   |
|   0
|
|
|
|
|
|
--------
""",
"""
-----
|   |
|   0
|  -+-
|
|
|
|
|
--------
""",
"""
-----
|   |
|   0
| /-+-
|
|
|
|
|
--------
""",
"""
-----
|   |
|   0
| /-+-\ 
|
|
|
|
|
--------
""",
"""
-----
|   |
|   0
| /-+-\ 
|   | 
|
|
|
|
--------
""",
"""
-----
|   |
|   0
| /-+-\ 
|   | 
|   | 
|
|
|
--------
""",
"""
-----
|   |
|   0
| /-+-\ 
|   | 
|   | 
|  |
|
|
--------
""",
"""
-----
|   |
|   0
| /-+-\ 
|   | 
|   | 
|  | 
|  | 
|
--------
""",
"""
-----
|   |
|   0
| /-+-\ 
|   | 
|   | 
|  | | 
|  | 
|
--------
""",
"""
-----
|   |
|   0
| /-+-\ 
|   | 
|   | 
|  | | 
|  | | 
|
--------
""")

        print(HANGMAN[0])
        attempts = len(HANGMAN) - 1


        while (attempts != 0 and "-" in word_guessed):
            print(("\nYou have {} attempts remaining").format(attempts))
            joined_word = "".join(word_guessed)
            print(joined_word)

            try:
                player_guess = str(input("\nPlease select a letter between A-Z" + "\n> ")).lower()
            except: # check valid input
                print("That is not valid input. Please try again.")
                continue                
            else: 
                if not player_guess.isalpha(): # check the input is a letter. Also checks an input has been made.
                    print("That is not a letter. Please try again.")
                    continue
                elif len(player_guess) > 1: # check the input is only one letter
                    print("That is more than one letter. Please try again.")
                    continue
                elif player_guess in guessed_letters: # check it letter hasn't been guessed already
                    print("You have already guessed that letter. Please try again.")
                    continue
                else:
                    pass

            guessed_letters.append(player_guess)

            for letter in range(len(chosen_word)):
                if player_guess == chosen_word[letter]:
                    word_guessed[letter] = player_guess # replace all letters in the chosen word that match the players guess

            if player_guess not in chosen_word:
                attempts -= 1
                print(HANGMAN[(len(HANGMAN) - 1) - attempts])

        if "-" not in word_guessed: # no blanks remaining
            print(("\nCongratulations! {} was the word").format(chosen_word))
        else: # loop must have ended because attempts reached 0
            print(("\nUnlucky! The word was {}.").format(chosen_word))

        print("\nWould you like to play again?")

        response = input("> ").lower()
        if response not in ("yes", "y"):
            play_again = False

if __name__ == "__main__":
    main()
\$\endgroup\$
1
  • \$\begingroup\$ Nice validation and well done with the play again question! You could break it into multiple functions, draw the hangman programmatically, download a full dictionary, add emoji. See sources for richardsgames.com/hangman.html \$\endgroup\$
    – fmalina
    Sep 27, 2018 at 15:51

4 Answers 4

9
\$\begingroup\$

You have a bare except clause; i.e.,

try:
     some_code()
except:
     clean_up()

The problem with a bare except is that it will catch all exceptions, including ones you really don’t want to be ignoring (like KeyboardInterrupt and SystemExit). It would be much better if your except block only caught the specific exception you expect, and let all others bubble up as normal.

A few other general comments on your code:

  • In line 200, you have this construction:

    for letter in range(len(chosen_word)):
        if player_guess == chosen_word[letter]:
            word_guessed[letter] = player_guess
    

    You’re looping over the index variable, but also using the list element. It would be better to write:

    for idx, letter in enumerate(chosen_word):
        if player_guess == letter:
            word_guessed[idx] = player_guess
    
  • Rather than using the flag value play_again, you could use a break statement. That is, structure your code as follows:

    while True:
        # play the game
    
        if not player_response_is_play_again():
            break
    

    That’s a bit cleaner and easier to follow, because I don’t have to follow the play_again variable around.

  • Your main() function is huge. Most of this code is not reusable, because it’s tied up in a single monolithic function. You’d be much better off breaking it into small, distinct units of code: those are easier to test and debug, and it would be more reusable to boot.

\$\endgroup\$
2
  • \$\begingroup\$ When you changed the loop, your "letter" is now an element of the list, instead of an index. You need to change the way the word_guessed data structure is used, if you're going to assign something new to it. This is not always cleaner when you consider the big picture. \$\endgroup\$ Jul 6, 2015 at 20:09
  • \$\begingroup\$ @ShreevatsaR Good spot, my mistake. Fixed. \$\endgroup\$
    – alexwlchan
    Jul 6, 2015 at 20:10
5
\$\begingroup\$

You have way too much code in your main function. Try and see if you can separate the logic out into separate functions, each with their own arguments. For example, the below code block:

words = ["hangman", "chairs", "backpack", "bodywash", "clothing",
                 "computer", "python", "program", "glasses", "sweatshirt",
                 "sweatpants", "mattress", "friends", "clocks", "biology",
                 "algebra", "suitcase", "knives", "ninjas", "shampoo"
                 ]

chosen_word = random.choice(words).lower()

Could easily be separated into a function named choose_word, like this:

def choose_word(word_list):
    """
    Choose a random word from the word_list
    and return it.
    """
    return random.choice(word_list)

You also have some long mutliline strings. For example, the following multiline string:

"""
-----
|   |
|
|
|
|
|
|
|
--------
"""

Can very easily be shortened to this:

"-----\n|   |\n|\n|\n|\n|\n|\n|\n|\n--------"

Do note, this isn't required though. It's just personal preference.


The way you're printing multiple pieces of text is odd, as well. For example, the below piece of code:

welcome = ['Welcome to Hangman! A word will be chosen at random and',
               'you must try to guess the word correctly letter by letter',
               'before you run out of attempts. Good luck!'
               ]

    for line in welcome:
        print(line, sep='\n')

Can again be shortened to this:

print(
    "Welcome to Hangman! A word will be chosen at random and",
    "you must try to guess the word correctly letter by letter",
    "before you run out of attempts. Good luck!",
    sep="\n"
)

I also noticed that you're mixing the use of single quotes, '', and double quotes, "". While this isn't a huge deal, I'd recommend choosing one or the other, and sticking with it.


The way you're styling lists is odd as well. While it isn't required, I'd recommend doing it like this:

my_list = [
    value,
    ...
]

Finally, when getting user input, you don't need to convert it to a string using the str function. You can just get it like this: user_input = input("prompt").

\$\endgroup\$
4
  • 9
    \$\begingroup\$ That "ugly mutliline string" is much more readable, as you actually get to see the real output \$\endgroup\$ Jul 6, 2015 at 18:15
  • \$\begingroup\$ @SimonAndréForsberg I've edited my post to include that as optional. \$\endgroup\$ Jul 6, 2015 at 18:17
  • \$\begingroup\$ Some good points here, but I disagree with condensing the string. \$\endgroup\$
    – user34073
    Jul 6, 2015 at 18:24
  • \$\begingroup\$ @SirPython Nope. Try the example. It works fine. \$\endgroup\$ Jul 6, 2015 at 18:27
4
\$\begingroup\$
for line in welcome:
    print(line, sep='\n')

This looks like the kind of code that would be in a function to print out multiple strings in array. However, since you only have one message that is in an array, this seems kind of overkill, especially since there are only three lines.

It would be a lot easier just to print out the message as it is:

print "..."
print "..."
...

Instead of trying to get your own list of words, I recommend trying one of these three options:

  • Find an API.

There are probably tons of API's on the internet that will serve as a dictionary for your program.

Pro: You don't have to download anything; just make a simple call to an API.

Con: APIs have limits such as calls per day. Also, sometimes your code can get hung up on an API call.

Unix words in a dictionary that comes with Unix and Unix-like operating systems.

Pro: It's just a file on the computer that has a single word per line, making it very easy to read.

Con: Your user may not be using Unix or a Unix-like operating system.

  • Use PyEnchant.

I don't know about this one. I saw it being used in some Python code before that involved an English words reference, but I don't know how accessing it's built-in dictionary works.


chosen_word = random.choice(words).lower()

There is no point in calling the method .lower because all the words in your dictionary are already lower-case.


When asking if the user wants to play again:

print("\nWould you like to play again?")

Instead of having to have a conditional where you check for two options later ("yes" and "y"), give the user their options:

print("\nWould you like to play again? [Y/N]")

And, to take it step further and get rid of an extra function call, put this message inside the input call:

input("\nWould you like to play again? [Y/N]\n>")

The variable welcome in the beginning of your code:

welcome = ['Welcome to Hangman! A word will be chosen at random and',
           'you must try to guess the word correctly letter by letter',
           'before you run out of attempts. Good luck!'
           ]

Should not be in an array; it should be in a tuple because the values in it are not going to change.

The same goes to your words array.


To answer your question about exceptions, I think that you should only use try/exception if you know that there will be errors. It is always good to catch and stop an error, but there is no point in trying if there will be no error.

Also, knowing that there will be errors helps because then you can catch the right kind of error, rather than just catching Exception.

You did a good job catching the error here and differing between using try/except and if/else:

try:
    player_guess = str(input("\nPlease select a letter between A-Z" + "\n> ")).lower()
except: # check valid input
    print("That is not valid input. Please try again.")
    continue 

However, you should be catching the type of error that would come out of that line.

I'm not sure which error will come out, but I think it is either a TypeError or an IOError. Either way, you'd change your try/except to something like this:

try:
    ...
catch ExceptionType:
    ...
\$\endgroup\$
3
\$\begingroup\$

You asked about your Exception Handling.

        try:
            player_guess = str(input("\nPlease select a letter between A-Z" + "\n> ")).lower()
        except: # check valid input
            print("That is not valid input. Please try again.")
            continue                
        else: 
            if not player_guess.isalpha(): # check the input is a letter. Also checks an input has been made.
                print("That is not a letter. Please try again.")
                continue
            elif len(player_guess) > 1: # check the input is only one letter
                print("That is more than one letter. Please try again.")
                continue
            elif player_guess in guessed_letters: # check it letter hasn't been guessed already
                print("You have already guessed that letter. Please try again.")
                continue
            else:
                pass

you first run the input through to see if it is a string and then say that it isn't valid input, and then you check for whether or not the character is an alpha character. You should just check whether or not it is an alpha character first, then you could eliminate the need for the try/catch.

It is either a letter or it isn't, right?

\$\endgroup\$

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