# Hangman program w/ Python and JSON

What improvements can I make? Should I organize my functions based on A-Z? For example, find_letter function comes before guessing function.

import json
import random
import re

def find_letter(user_input, vocab, i=None):
return [i.start() for i in re.finditer(user_input, vocab)]

def guessing(tries,vocab,blank_line):
while tries > 0:
user_input = input("Guess:")

if user_input in vocab:
if not(user_input in blank_line):
index = find_letter(user_input=user_input,vocab=vocab)
i = 0
while i < len(index):
blank_line[index[i]] = user_input
i += 1

if not('_' in blank_line):
return "you won!!!"
else:
else:
tries -= 1

print(str(blank_line) + "  Number of Tries : " + str(tries))

return "you lost"

def hangman_game(vocab):
'Preliminary Conditions'
print(introductory(vocab=vocab))
tries = num_of_tries(vocab=vocab)
blank_line = produce_blank_lines(vocab=vocab)
print(blank_line)

print(guessing(tries=tries,vocab=vocab,blank_line=blank_line))

def introductory(vocab):
return "HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. " \
"WORD COUNT : " + str(len(vocab))

def num_of_tries(vocab):
if len(vocab) < 7:
return 8
elif 7 <= len(vocab) < 10:
return 10
else:
return 14

def produce_blank_lines(vocab):
blank_line = []
for i in vocab:
if i.isalpha():
blank_line += "_"
elif i == " ":
blank_line += " "
else:
blank_line += i + " "

return blank_line

if __name__ == '__main__':
vocab = random.choice(list(data.keys())).lower()
print(vocab)
print(hangman_game(vocab=vocab))


Snippet of the JSON File

{"abandoned industrial site": ["Site that cannot be used for any purpose, being contaminated by pollutants."], "abandoned vehicle": ["A vehicle that has been discarded in the environment, urban or otherwise, often found wrecked, destroyed, damaged or with a major component part stolen or missing."], "abiotic factor": ["Physical, chemical and other non-living environmental factor."], "access road": ["Any street or narrow stretch of paved surface that leads to a specific destination, such as a main highway."], "access to the sea": ["The ability to bring goods to and from a port that is able to harbor sea faring vessels."], "accident": ["An unexpected, unfortunate mishap, failure or loss with the potential for harming human life, property or the environment.", "An event that happens suddenly or by chance without an apparent cause."], "accumulator": ["A rechargeable device for storing electrical energy in the form of chemical energy, consisting of one or more separate secondary cells.\\n(Source: CED)"], "acidification": ["Addition of an acid to a solution until the pH falls below 7."], "acidity": ["The state of being acid that is of being capable of transferring a hydrogen ion in solution."], "acidity degree": ["The amount of acid present in a solution, often expressed in terms of pH."], "acid rain": ["Rain having a pH less than 5.6."]

• @ close-voter: Answers on this site are always "opinion-based." I don't even know why that's a close-vote reason. Anyway, this question is fine. Also to answer the OP's one explicit question: Almost certainly you should not arrange these six functions in a-z order. :) – Quuxplusone Dec 26 '18 at 23:54
• Please include an example (even tiny no problem) json file for easier testing. (The question is fine upvote from me) – Caridorc Dec 27 '18 at 0:17
• @Quuxplusone How do programmers organize functions then? – austingae Dec 27 '18 at 2:32
• @austingae How do programmers organize functions then? Typically by topic, and fine-grained enough that the order doesn't matter. For example you might put everything related to Hangman in hangman.py; or once that gets too big, you'd put everything related to the user interface in hangman-ui.py and everything related to core gameplay in hangman-core.py; or once that gets too big, you'd split out hangman-dictionary.py into its own file... And then sure, I'd probably sort those files alphabetically in my manifest or Makefile or whatever. – Quuxplusone Dec 27 '18 at 2:43
• It occurs to me that sorting functions by name has the dictionary problem: I can't find the function (say, introductory()) unless I already know how to spell it! And if I know how to spell it (say, my IDE has a sidebar index of all the functions in the current file) then I likely don't care where in the file it is (since I can jump there using my IDE). But everyone has an "IDE" for the filesystem (ls), and the filenames themselves make a rough "index by topic" — dictionary problem solved! – Quuxplusone Dec 27 '18 at 2:50

data = json.load(open("vocabulary.json"))


Unless Python 3 has changed this, I think you're leaking the open file handle here. What you want is more like

with open("vocabulary.json") as f:


which IMHO is even a bit easier to read because it breaks up the mass of nested parentheses.

vocab = random.choice(list(data.keys())).lower()


Same deal with the nested parentheses here. We can save one pair by eliminating the unnecessary materialization into a list. [EDIT: Graipher points out that my suggestion works only in Python 2. Oops!]

Also, vocab (short for vocabulary, a set of words) isn't really what this variable represents. data.keys() is our vocabulary; what we're computing on this particular line is a single vocabulary word. So:

word = random.choice(data.keys()).lower()
print(word)
print(hangman_game(word))


I can tell you're using Python 3 because of the ugly parentheses on these prints. ;) But what's this? Function hangman_game doesn't contain any return statements! So it basically returns None. Why would we want to print None to the screen? We shouldn't be printing the result of hangman_game; we should just call it and discard the None result.

word = random.choice(data.keys()).lower()
print(word)
hangman_game(word)


def hangman_game(vocab):


Same comment about vocab versus word here.

'Preliminary Conditions'


This looks like you maybe forgot a print? Or maybe you're trying to make a docstring? But single quotes don't make a docstring AFAIK, and even if they did, "Preliminary Conditions" is not a useful comment. I'd just delete this useless line.

print(introductory(vocab=vocab))


In all of these function calls, you don't need to write the argument's name twice. It's a very common convention that if you're calling a function with only one argument, the argument you pass it is precisely the argument it needs. ;) Also, even if the function takes multiple parameters, the reader will generally expect that you pass it the right number of arguments in the right order. You don't have to keyword-argument-ize each one of them unless you're doing something tricky that needs calling out. So:

print(introductory(word))


And then you can inline introductory since it's only ever used in this one place and it's a one-liner already:

print(
"HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
"WORD COUNT : " + str(len(vocab))
)


Personally, I would avoid stringifying and string-concatenation in Python, and write simply

print(
"HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
"WORD COUNT : %d" % len(vocab)
)


I know plenty of working programmers who would write

print(
"HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
"WORD COUNT : {}".format(len(vocab))
)


instead. (I find that version needlessly verbose, but it's definitely a popular alternative.)

tries = num_of_tries(vocab=vocab)
blank_line = produce_blank_lines(vocab=vocab)
print(blank_line)
print(guessing(tries=tries,vocab=vocab,blank_line=blank_line))


It's odd that you abbreviated number_of_tries to num_..., but then left in the word of. I would expect either number_of_tries or num_tries (or ntries for the C programmers in the audience); num_of_tries is in a weird no-man's-land.

It's weird that you have a function named produce_blank_lines that produces actually a single blank_line. It's even weirder that the value of blank_line is not a constant "\n"! This suggests that everything here is misnamed. You probably meant underscores = produce_underscores(word) or even blanks = shape_of(word).

Finally, it is weird that blank_line is not even a string, but rather a list of characters.

I would replace this entire dance with something like

blanks = ''.join('_' if c.isalpha() else ch for ch in word)


or

blanks = re.sub('[A-Za-z]', '_', word)


This would lose your addition of a blank space after each non-space-non-alpha character, but IMHO that's actually an improvement over your version. (Your sample JSON doesn't show any examples of words containing non-space-non-alpha characters. My version certainly performs better for words like non-space or Route 66.)

if not(user_input in blank_line):


This would be idiomatically expressed (both in Python and in English) as

if user_input not in blank_line:


index = find_letter(user_input=user_input,vocab=vocab)
i = 0
while i < len(index):
blank_line[index[i]] = user_input
i += 1


You have another singular/plural problem here (just like with blank_line[s]). It doesn't make sense to get the len of a singular index. Since the variable actually represents a list of indices, we should name it [list_of_]indices.

Also, this style of for-loop is very un-Pythonic. In Python we can say simply

for i in find_letter(user_input, word):
blank_line[i] = user_input


And then we can inline the single-use find_letter:

for m in re.finditer(user_input, word):
blank_line[m.start()] = user_input


(Using m as the conventional name for a match object.) And then we can eliminate the regex and just loop over the word directly:

for i, ch in enumerate(word):
if ch == user_input:
blank_line[i] = user_input


or

for i in range(len(word)):
if word[i] == user_input:
blank_line[i] = user_input


In fact, by not using regexes, we fix a bug in your code: if the chosen vocabulary word contains . as one of its non-space-non-alpha characters, a user who enters . as their guess will win the game immediately! :D

Anyway, that's enough for one answer, I think.

• Unfortunately your second recommendation produces the error TypeError: 'dict_keys' object does not support indexing when trying to randomly choose a key, so the wrapping with list is needed. In Python 2 this worked since the keys were a list already. – Graipher Dec 27 '18 at 10:13
• But you can get rid of the .keys(), random.choice(list(data)) works. – Graipher Dec 27 '18 at 10:15
• And nowadays f"WORD COUNT : {len(vocab)}" is all the rage, which is at least shorter again :) – Graipher Dec 27 '18 at 10:17
• Ah, I mistakenly thought random.choice could handle any iterable. If OP had written random.choice(list(data)) I probably would have complained that that approach was too "cute" and not expressive enough. ;) The problem of a random.choice that works on arbitrary iterables is discussed here: stackoverflow.com/questions/12581437/… – Quuxplusone Dec 27 '18 at 16:09
• If I have many variables to concatenate in a string, isn't it better to use +. But if I only have a variable to concatenate at the end of the string, then is it better to use %s or %d? – austingae Dec 28 '18 at 18:21