(Python) Random Number Guessing Game
A nice little guessing game you've got here.  Skill will only get you 70%, you still need luck to win.

Quoted Stings

The Python language gives you 4 different syntaxes for creating strings: "...", '...', """...""", and '''...'''.  The first two will allow you to embed quotes and double quotes, respectively, without escaping.  The latter two will allow you to embed either quote, as well as new-lines, without needing escapes.

            print("Sorry, you didn't type \"YES\" or \"NO\"...")


Here you want double quotes inside the string, and since you're using double quotes for your string, you've had to escape them.  If you had used a triple-quoted string:

            print('''Sorry, you didn't type "YES" or "NO"...''')


no escaping is necessary.

Code Organization

Your code presently looks like this:

import random

print("Welcome to the Guessing Game!")
user_name = input("What should I call you? ")
print("Hello, " + user_name)

def gameplay():
    ... # contents omitted for brevity

gameplay()


You've got imports, mainline code, a function definition, then more mainline code.  You should group all the mainline code together, not have it separated by other function definitions.

import random

def gameplay():
    ... # contents omitted for brevity

print("Welcome to the Guessing Game!")
user_name = input("What should I call you? ")
print("Hello, " + user_name)

gameplay()


Main Guard

It is highly recommended all mainline code be protected by a main guard:

import random

def gameplay():
    ... # contents omitted for brevity

if __name__ == '__main__':
    print("Welcome to the Guessing Game!")
    user_name = input("What should I call you? ")
    print("Hello, " + user_name)

    gameplay()


This allows the file to be imported by other modules.  You might think that this game code will never be imported by another module, because it is stand-alone code, but if you want to run any tests on the code, this file gets imported and the game starts to run messing up the test framework.  So always use this main guard.

Unnecessary Recursion

gameplay() calls restart_game().  Then restart_game() can call either gameplay() or restart_game().  Both of these are recursive calls, which add more and more frames to the program stack.  Eventually, the Python program stack will exceed its maximum limit and the program will crash.  The user will likely get bored long before the stack overflows, but some automated testing script which is testing various guessing strategies might play thousands of games within a second, and there the program would crash.

Note: Some languages utilize something called "Tail Call Optimization" (TCO) and optimize out these tail-recursive calls.  Python is not one of them; it doesn't do TCO.

Each of these recursive calls is easily replaced by a simple loop.  Let's start with the inner loop:

Do you want to play again?

This is a simple YES/NO question.  Many programs might need it (although they might not exactly phrase the question about a game.  You hid the function inside the gameplay() function, which was fine because it was tightly coupled to that outer function, but making it more general means we'll want it moved out to become its own top-level function:

def yes_or_no(prompt: str) -&gt; bool:
    """
    Ask a "yes or no" question.

    The question will be repeated until the user responds with "YES" or "NO",
    but the user doesn't need to capitalize their response.

    Parameters:
        prompt: the yes/no question to ask the user.

    Returns:
        ``True`` if the user responds "YES", ``False`` if the user responds "NO".
    """

    user_reply = input(prompt).upper()
    while user_reply not in {"YES", "NO"}:
        print('Sorry, you didn't type "YES" or "NO")
        user_reply = input(prompt).upper()

    return user_reply == "YES"


A couple of points to highlight:


Type hints (eg, prompt: str, and -&gt; bool) are optional, but very useful.
"""docstrings""" are also very useful.  If you run your program in an REPL (such as IDLE), type help(yes_or_no) at the &gt;&gt;&gt; prompt, after the program has run.
The user input is converted to uppercase once, immediately after input() returns a value.  It is not converted to uppercase on each test (eg, user_reply.upper() == "YES" and user.reply.upper() == "NO")
The while-loop repeats the question, if invalid input is given.  No recursion.
The user_reply not in { ... } is an efficient way of testing for an invalid response.


Using the yes_or_no() reply

If the user answers "YES", your code recursively called gameplay() for another game.  Now that we have our yes_or_no() question function, let's use it and eliminate that recursion.  Let's refactor the mainline code into a guessing_game() function in the process:

import random

def gameplay():
    ... # contents omitted for brevity

def guessing_game():
    print("Welcome to the Guessing Game!")
    user_name = input("What should I call you? ")
    print("Hello, " + user_name)

    play_again = True
    while play_again:
        gameplay()
        play_again = yes_or_no("Do you want to play again? " +
                               "Type YES if you want to play again or NO if you want to quit... "):

    print("Script terminating. Exiting game...")

if __name__ == '__main__':


Now, we are calling gameplay() in a loop, instead of recursively.

The Guessing Loop

    while user_guess != number and not(out_of_guesses):
        if no_of_guesses &lt; guess_limit:
            user_guess = ...
            ...
            no_of_guesses += 1
        else:
            out_of_guesses = True


This loop makes my head hurt.  You are looping once per guess, to a maximum of guess_limit guesses, plus one additional loop iteration to set out_of_guesses = True to terminate the loop.  It is that extra iteration that is really bizarre.  It works, but ... (shudder).

Let's try a completely different loop structure.  We have a guess_limit; let's make a loop based on that:

    for guess_number in range(guess_limit):
        user_guess = int(input("Guess the number from 1 to 10 that I'm thinking of... "))


That alone will ask the user to guess 3 times.  If they guess the number, we want to break out of the loop early.

    for guess_number in range(guess_limit):
        user_guess = int(input("Guess the number from 1 to 10 that I'm thinking of... "))

        if user_guess == number:
            print("You guessed it! Congratulations, you win the game!")
            break


But what about when they fail to guess it?  If the for loop finishes all iterations without ever break-ing out of the loop, it will execute an optional else: clause:

    for guess_number in range(guess_limit):
        user_guess = int(input("Guess the number from 1 to 10 that I'm thinking of... "))

        if user_guess == number:
            print("You guessed it! Congratulations, you win the game!")
            break

    else:
        print("You ran out of guesses, try again")


The complete function, with the low/high guessing hints added back in:

def gameplay(guess_limit=3):
    """
    A number guessing game.

    A random integer will be selected between 1 and 10.  You have to guess the
    number within the allotted number of guesses.

    Parameters:
        guess_limit: Number of guesses to allow.  Allow more guesses for an easier game 
    """

    number = random.randint(1, 10)

    for guess_number in range(guess_limit):
        user_guess = int(input("Guess the number from 1 to 10 that I'm thinking of... "))

        if user_guess == number:
            print("You guessed it! Congratulations, you win the game!")
            break

        if user_guess &lt; number:
            print("Your number was too low...")
        else:
            print("Your number was too high...")

    else:
        print("You ran out of guesses, try again")


I've made guess_limit a parameter, with a default of 3, to demonstrate another Python feature: default arguments.  gameplay() will play the game as normal, but you could call the function with gameplay(4) to make an easier variant of the game.