Python Bulls and Cows Bulls and Cows is a simple code-breaking game sometimes better known as "Mastermind." The rules are explained here: https://en.wikipedia.org/wiki/Bulls_and_Cows

I would appreciate some feedback on the following implementation in terms of overall code quality, layout and logic. It's meant to be at a fairly basic level so I don't want to over complicate things, but want to avoid any obviously bad choices. Suggestions for improvements very welcome.

import random

SECRET_NUMBER_SIZE = 2
MAX_GUESSES = 5

# Generate secret number with SECRET_NUMBER_SIZE digits and no duplicates
secret_number = ""
while len(secret_number) &lt; SECRET_NUMBER_SIZE:
    new_digit = str(random.randint(0, 9))
    if new_digit not in secret_number:
        secret_number += new_digit

# print(f"For testing. Secret number is: {secret_number}")

print(f"Guess my number. It contains {SECRET_NUMBER_SIZE}\
unique digits from 0-9")

remaining_turns = MAX_GUESSES

while remaining_turns &lt;= MAX_GUESSES:

    # Get user guess and validate length
    player_guess = input("Please enter your guess: ")
    if len(player_guess) != SECRET_NUMBER_SIZE:
        print(f"Your guess must be {SECRET_NUMBER_SIZE} digits long.")
        continue

    # Main game logic
    if player_guess == secret_number:
        print("Yay, you guessed it!")
        break
    else:
        bulls = 0
        cows = 0
        for i in range(SECRET_NUMBER_SIZE):
            if player_guess[i] == secret_number[i]:
                bulls += 1
        for j in range(SECRET_NUMBER_SIZE):
            if player_guess[j] in secret_number and \
              player_guess[j] != secret_number[j]:
                cows += 1
        print(f"Bulls: {bulls}")
        print(f"Cows: {cows}")
        remaining_turns -= 1
        if remaining_turns &lt; 1:
            print("You lost the game.")
            break
@Josay's and @Reinderien's answers both give useful advice, with @Josay's being particularly comprehensive. However, there is one point that I've noticed neither of them have covered

Generation of the secret number

# Generate secret number with SECRET_NUMBER_SIZE digits and no duplicates
secret_number = ""
while len(secret_number) &lt; SECRET_NUMBER_SIZE:
    new_digit = str(random.randint(0, 9))
    if new_digit not in secret_number:
        secret_number += new_digit


This is an inelegant way of generating the secret number for two reasons:


You concatenate to a string, an immutable type. This is much more expensive than necessary, which does not a notable impact on a program of this size, but could become problematic as you scale up; it's better to practice good techniques, even at small scales. Additionally, you should never need to iteratively concatenate to a string in situations where the string's contents are generated all at once, because there are two successively more efficient ways:


Since Python uses dynamic lists, you can create a list with the string's contents, and then use "".join(str(i) for i in src_lst)
To improve the efficiency even further, you can utilize the fact that you know the list's size already, and "pre-allocate" the list using lst = [None] * SECRET_NUMBER_SIZE and then iteratively fill in its contents using a numerical index variable. This is a bit unpythonic, as @Josay goes over in their answer, and is probably a bit of premature optimization, but it's a good trick to have up your sleave if you want to eek out a bit more performance, and may be extremely useful depending on the situation.
But there's an even better way, which we'll get to in a second...

Every number is checked to ensure it hasn't already occurred in the result list. The reason this is not ideal is because at larger scales, one could end up generating many numbers before they stumble upon one that hasn't already been chosen. To mitigate this, one could lower the generation range each time, and offset the numbers accordingly, but it's probably not worth the effort because this isn't much of a practical concern, even at larger scales; generating random numbers using the Mersenne Twister is very cheap. But this concern helps lead to an elegant solution...


random.sample() is your friend here. It allows you to generate multiple distinct random numbers quite easily; all you have to provide is a population sequence and the size of the sample. For this situation, a population sequence is quite simple: range(10) supplies all the numbers between 0 and 9 inclusive. You can now simplify your 5 lines above into ~1 line:

secret_number = "".join((str(i) for i in
    random.sample(range(10), SECRET_NUMBER_SIZE)))