Tic Tac Toe / Noughts and Crosses - First Program
Seems pretty nicely done. Most of the following are minor improvements, but if in case you do not get any of the following parts, do let me know. COnsidering the beginner tag, I have written a small appendix at the end. Suggestions for improvements:


The Format method: Instead of using the print() as you have written, it is better to use a format() method in general. For your purposes, it might be overkill, but in case you are incorporating some of the points below (especially 5), it might come in handy for a pretty output.
Alternative for if-elif-else: The entire block from try to the final else can be replaced by regexsearch (or any equivalent string search) to look for correct responses and club the not a number and incorrect number error messages into one. One suggestion for that is below.


Suggestion code:

# initialize with grid = ["0","1","2","3","4","5","6","7","8"]
if (p1_input not in grid) or (p1_input in ['x','o']):
    print('Combined Error Message')
else:
    grid[int(p1_input)]='x'
    draw_grid()



Checking for victory: Instead of listing down all possible victory positions, the bulky code can be avoided. Although I am currently unable to come up with anything simpler than the following code which uses numpy, you can at least contract the current code in a single function that can be called for both p1 and p2, reducing the bulk of that code by half. The benefit of using the numpy variant is, it is size-independent. You can update a for a 4x4 game and it should be directly usable.


Numpy Variant:

def p1_check():
    # a defines a list of all winning positions
    a = [(0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), (0, 4, 8), (2, 4, 6)]
    # &lt;= is the subset operator for sets. Additionally, the where statement finds the indices for all 'x'
    l = list(numpy.where(numpy.array(grid) == 'x')[0])
    if True in list(map(lambda x: (set(x) &lt;= set(l)),a)):
        print('Victory')
        return "End Game"


Contraction of your code:

def check(player):
   a = ['x','o'][player-1]
   if grid[0] == a and grid[1] == a and grid[2] == a or \
         grid[3] == a and grid[4] == a and grid[5] == a or \
         grid[6] == a and grid[7] == a and grid[8] == a or \
         grid[0] == a and grid[3] == a and grid[6] == a or \
         grid[1] == a and grid[4] == a and grid[7] == a or \
         grid[2] == a and grid[5] == a and grid[8] == a or \
         grid[6] == a and grid[4] == a and grid[2] == a or \
         grid[0] == a and grid[4] == a and grid[8] == a:
            print([p1_name,p2_name][player-1] + " Wins!")
            return "End Game" 
#call check(1) and check(2) appropriately



Earlier Ties: In order to account for configurations where a tie is definite, without all the boxes having been filled, you can work out if there is any winning chance considering only all the 'x' and the empty boxes (same for 'o'). To do this, one way is to use the numpy variant above with a slight modification. This matters very little in a 3x3 game but ismore relevant for larger games.


Suggestion code:

# a defines a list of all winning positions
def tie_check():
    a = [(0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), (0, 4, 8), (2, 4, 6)]
    # using != instead of ==, 'or' combining the lists
    canwin = (True in list(map(lambda x: set(x) &lt;= set(list(numpy.where(numpy.array(grid) != 'x')[0])),a))) or \
          (True in list(map( lambda x: set(x) &lt;= set(list(numpy.where(numpy.array(grid) != 'o')[0])),a)))
    if not canwin:
        print("Game is a tie")
        return "End Game"



Multiple Games and statistics: To just improve the usability of the code, you can allow for specifying at the end whether to play another game or not. Provision for a best of 3/best of 5 can easily be incorporated in the play() function with an additional option of storing and printing the statistics for the game. 


APPENDIX

Numpy is basically used for its arrays and array-methods which are very fast and convenient. For our purposes, only two of these are relevant. numpy.where just returns an array of indices corresponding to True. Whenever a numpy array is equated (==) to any value, an array of booleans is returned corresponding to element-wise matching. I urge you to try out with self-made arrays and it is very easy to get hang of, for 1-D case, which is the one used above.

Map is just a way to apply a function element-wise to a list and return a list of values returned for every such application. I assume you are familiar with lambda(a way to make functions on the fly) and set which are very similar to the mathematical entity.