I am currently learning object oriented programming in Python. I attempted writing an OOP program for hangman, and while the program is functional, I just ended up writing a very long class method. Do you have any suggestions on how to improve the code to make it more pythonic, and some resources that can help me drill-in the concepts?
main.py
import random
from words import word_list
from question import Word
from art import logo
import random
print(logo)
new_word = Word(random.choice(word_list))
#print(new_word.word)
word_length = len(new_word.word)
placeholder = ""
for position in range(word_length):
placeholder += "_"
print("Word to guess: " + placeholder)
while new_word.lives != 0:
new_word.check_word()
print(f"You lose! The correct word was '{new_word.word}'.")
question.py
from art import stages
class Word:
def __init__(self,word):
self.word = word
self.lives = 6
self.correct_letters = []
def check_word(self):
guess = input(f"Guess the letter: ").lower()
if guess in self.correct_letters:
print(f"You've already guessed {guess}")
display = ""
for i in self.word:
if i == guess:
print("That's right")
display += i
self.correct_letters.append(i)
elif i in self.correct_letters:
display += i
else:
display += "_"
print("Word to guess: " + display)
if guess not in self.word:
self.lives -= 1
print(f"You guessed {guess}, that's not in the word. You lose a life.")
print(f"You have {self.lives} left.")
print(stages[self.lives])
2 Answers 2
Comments
Delete all commented-out code:
#print(new_word.word)
Simpler
The variable named position
is unused:
for position in range(word_length):
Typically, the underscore placeholder is used:
for _ in range(word_length):
In fact, all of these lines:
word_length = len(new_word.word)
placeholder = ""
for position in range(word_length):
placeholder += "_"
print("Word to guess: " + placeholder)
can be replaced with one line:
print("Word to guess: " + "_" * len(new_word.word))
This eliminates 2 intermediate variables.
The f-string is not needed here:
guess = input(f"Guess the letter: ").lower()
It is simpler as:
guess = input("Guess the letter: ").lower()
Documentation
The PEP 8 style guide recommends adding docstrings for classes and functions.
For example:
def check_word(self):
"""
Gets an input letter from the user.
Check to see if it is correct or if it has been guessed already.
Print result.
"""
Import
This line appears twice in file main.py
:
import random
Delete one of the import
lines.
Portability
I just installed the art
module, but I get this error when I try to run the code:
ImportError: cannot import name 'stages' from 'art'
Perhaps we have different versions of Python and/or art
.
Efficiency
You have several opportunities to make your code more efficient and Pythonic:
Where you have:
placeholder = ""
for position in range(word_length):
placeholder += "_"
print("Word to guess: " + placeholder)
I would suggest instead:
print("Word to guess: ", "_" * word_length)
This is elimimates all unnecessary string concatenations.
Where you have:
while new_word.lives != 0:
new_word.check_word()
I would suggest instead the more Pythonic:
while new_word.lives:
new_word.check_word()
Instead of self.correct_letters
being a list
instance, I would make it a set
instance. This makes checking a user's guess against letters previously entered more efficient.
Improved Logic
In main.py you have:
while new_word.lives != 0:
new_word.check_word()
print(f"You lose! The correct word was '{new_word.word}'.")
Eventually you will fall through the while
loop and tell the user they have lost even if they have won.
Another issue is that you have a class Word
, but the client of this class needs to know too much about its internal representation, i.e. state attributes lives
. It would be better if the classes implementation does not have to be exposed to a client (data hiding).
Your class is also keeping track of previously entered correct guesses (letters). You need to keep instead just a set of previously entered guesses whether they were correct or not. Otherwise, your printing of f"You've already guessed {guess}"
will not report on previously entered incorrect guesses.
Here is an example updated class. Note that I have NOT installed the word
or art
package, so my implementation will be a bit different. Note also that the classes attributes begin with a leading underscore to suggest to a client that the attribute is "private."
class Word:
def __init__(self, word):
self._word = word
self._lives = 6
self._guesses = set()
def _display(self) -> bool:
"""A private method to display current game state and return True
if the game has been solved."""
lnth = len(self._word)
display = [None] * lnth
solved = True
for idx, letter in enumerate(self._word):
if letter in self._guesses:
display[idx] = letter
else:
display[idx] = '_'
solved = False
print()
print(' '.join(display))
return solved
def play_game(self):
"""Play the game."""
self._display()
while True:
guess = input("Guess the letter: ").lower()
if guess in self._guesses:
print(f"You've already guessed {guess}")
else:
self._guesses.add(guess)
if guess not in self._word:
print("That is an incorrect guess.")
else:
print("That is a correct guess!")
solved = self._display()
if solved:
print('You have won!')
break
self._lives -= 1
if self._lives == 0:
print("I am sorry, but you have lost.")
break
print(f"You have {self._lives} guesses left.")
if __name__ == '__main__':
new_word = Word('ghost')
new_word.play_game()
-
1\$\begingroup\$ I don't think
play_game
should be a method ofWord
. It should be a separateGame
class or a standaloneplay_game
function, but not a method ofWord
- that would violently break separation of concerns. Or that class should not be namedWord
- your class is very similar toGame
, andWord
might be unnecessary abstraction on top ofstr
. \$\endgroup\$STerliakov– STerliakov2025年07月06日 19:07:49 +00:00Commented Jul 6 at 19:07 -
\$\begingroup\$ @STerliakov My understanding was that
Word
was short forWord_Game
. \$\endgroup\$Booboo– Booboo2025年07月06日 21:28:53 +00:00Commented Jul 6 at 21:28
Explore related questions
See similar questions with these tags.
Word
class is to play a game of hangman, then I would expect it to be namedHangman
. The class nameWord
does not match with the intended usage. \$\endgroup\$