Tic Tac Toe Game in Python (With Unbeatable AI)

A tic tac toe game in Python needs three pieces: a list of nine squares for the board, a function that checks the eight winning lines, and a loop that asks each player for a move. The winner check is the heart of it:

WINS = [(0,1,2), (3,4,5), (6,7,8), (0,3,6), (1,4,7), (2,5,8), (0,4,8), (2,4,6)]

def winner(board):
    for a, b, c in WINS:
        if board[a] == board[b] == board[c] != " ":
            return board[a]

I really played the game below in Command Prompt on Python 3.12.5, typing each move, including a couple of bad ones to test the input checks.

You’ll draw the board, check for a winner, build the two-player game with input validation, then add a computer opponent you can’t beat.

Tic tac toe board and winner check in Python

You can store the board as a plain list of nine strings. Index 0 is the top-left square and 8 is the bottom-right. The winner function checks every row, column and diagonal:

WINS = [(0, 1, 2), (3, 4, 5), (6, 7, 8),      # rows
        (0, 3, 6), (1, 4, 7), (2, 5, 8),      # columns
        (0, 4, 8), (2, 4, 6)]                 # diagonals

def show(board):
    rows = [" | ".join(board[i:i + 3]) for i in (0, 3, 6)]
    print("\n---------\n".join(rows), "\n")

def winner(board):
    for a, b, c in WINS:
        if board[a] == board[b] == board[c] != " ":
            return board[a]
    return "Draw" if " " not in board else None

board = ["X", "O", "X",
         " ", "X", "O",
         "O", " ", "X"]
show(board)
print("winner:", winner(board))
print("empty squares:", [i + 1 for i, s in enumerate(board) if s == " "])

Output:

X | O | X
---------
  | X | O
---------
O |   | X 

winner: X
empty squares: [4, 8]
Command Prompt output of a Python tic tac toe board printed with separators and the winner check returning X
X wins on the diagonal, squares 1, 5 and 9. The empty squares are listed as 4 and 8.

Returning “Draw” when there are no empty squares, and None while the game is still going, gives your main loop one value to check.

Tic tac toe Python code: the two-player game

Your game loop alternates between X and O until winner() returns something. The ask() function keeps asking until the player types a valid, empty square:

WINS = [(0, 1, 2), (3, 4, 5), (6, 7, 8),      # rows
        (0, 3, 6), (1, 4, 7), (2, 5, 8),      # columns
        (0, 4, 8), (2, 4, 6)]                 # diagonals

def show(board):
    rows = [" | ".join(board[i:i + 3]) for i in (0, 3, 6)]
    print("\n---------\n".join(rows), "\n")

def winner(board):
    for a, b, c in WINS:
        if board[a] == board[b] == board[c] != " ":
            return board[a]
    return "Draw" if " " not in board else None

def ask(board, player):
    while True:
        choice = input(f"Player {player}, pick a square (1-9): ").strip()
        if not choice.isdigit() or not 1 <= int(choice) <= 9:
            print("Please type a number from 1 to 9.")
        elif board[int(choice) - 1] != " ":
            print("That square is taken, try another.")
        else:
            return int(choice) - 1

def play():
    board = [" "] * 9
    player = "X"
    show([str(i) for i in range(1, 10)])        # show the square numbers once
    while winner(board) is None:
        board[ask(board, player)] = player
        show(board)
        player = "O" if player == "X" else "X"
    result = winner(board)
    print("It's a draw!" if result == "Draw" else f"Player {result} wins!")

play()

Output:

1 | 2 | 3
---------
4 | 5 | 6
---------
7 | 8 | 9 

Player X, pick a square (1-9): 1
X |   |  
---------
  |   |  
---------
  |   |   

Player O, pick a square (1-9): 4
X |   |  
---------
O |   |  
---------
  |   |   

Player X, pick a square (1-9): 4
That square is taken, try another.
Player X, pick a square (1-9): 2
X | X |  
---------
O |   |  
---------
  |   |   

Player O, pick a square (1-9): 5
X | X |  
---------
O | O |  
---------
  |   |   

Player X, pick a square (1-9): ten
Please type a number from 1 to 9.
Player X, pick a square (1-9): 3
X | X | X
---------
O | O |  
---------
  |   |   

Player X wins!
Command Prompt showing a two-player tic tac toe game in Python with typed moves, a taken-square warning and Player X winning
Square 4 was already taken and “ten” isn’t a number, so the game asked again. X wins across the top row.

The validation uses isdigit() and a range check, so a typo can’t crash your game. The while loop pattern is covered in the Python while loop guide.

How do you make an unbeatable tic tac toe AI in Python?

If you want a computer opponent, minimax is the classic answer: it tries every possible future move and assumes both players play perfectly. O picks the move with the highest score, X the lowest. Caching positions with lru_cache makes it instant:

WINS = [(0, 1, 2), (3, 4, 5), (6, 7, 8),      # rows
        (0, 3, 6), (1, 4, 7), (2, 5, 8),      # columns
        (0, 4, 8), (2, 4, 6)]                 # diagonals

def show(board):
    rows = [" | ".join(board[i:i + 3]) for i in (0, 3, 6)]
    print("\n---------\n".join(rows), "\n")

def winner(board):
    for a, b, c in WINS:
        if board[a] == board[b] == board[c] != " ":
            return board[a]
    return "Draw" if " " not in board else None

import random
from functools import lru_cache

@lru_cache(maxsize=None)
def minimax(board, player):
    """Return (score, move) for player; +1 means O wins, -1 means X wins."""
    result = winner(list(board))
    if result == "O": return 1, None
    if result == "X": return -1, None
    if result == "Draw": return 0, None
    best = None
    for i in range(9):
        if board[i] == " ":
            nxt = board[:i] + (player,) + board[i + 1:]
            score, _ = minimax(nxt, "X" if player == "O" else "O")
            if best is None or (player == "O" and score > best[0]) or (player == "X" and score < best[0]):
                best = (score, i)
    return best

def computer_move(board):
    return minimax(tuple(board), "O")[1]

random.seed(2026)
results = {"X": 0, "O": 0, "Draw": 0}
for _ in range(300):
    board = [" "] * 9
    player = "X"
    while winner(board) is None:
        if player == "X":
            move = random.choice([i for i, s in enumerate(board) if s == " "])
        else:
            move = computer_move(board)
        board[move] = player
        player = "O" if player == "X" else "X"
    results[winner(board)] += 1

print("300 games, random X vs minimax O:", results)
print("positions cached:", minimax.cache_info().currsize)

Output:

300 games, random X vs minimax O: {'X': 0, 'O': 250, 'Draw': 50}
positions cached: 5477
Command Prompt output of a Python minimax tic tac toe AI playing 300 games against random moves without losing once
Across 300 games the minimax player never lost. The cache stored a few thousand positions.

Tic tac toe is small enough to search completely, which is why perfect play always ends in a draw. To hook it into your game, call computer_move(board) on O’s turn instead of ask(). Recursion in Python explains how minimax calls itself.

Ideas to extend your tic tac toe game

  • Let the player choose X or O, and who goes first.
  • Keep a running score across rounds with a dictionary.
  • Add a difficulty setting: random moves for easy, minimax for hard.
  • Build a window version with Tkinter buttons; see Tkinter button commands.

If you liked building this, try these next:

Frequently asked questions

How do you make a tic tac toe game in Python?

Store the board in a list of nine squares, write a function that checks the eight winning lines, and loop over player turns using input() until there’s a winner or a draw.

How do I check for a winner in tic tac toe?

List the eight winning index triples and check whether all three squares in any triple hold the same non-empty mark.

How do I stop players from choosing a taken square?

Check board[index] != " " before placing the mark, and ask again inside a while True loop.

Can the computer play tic tac toe perfectly?

Yes. Minimax searches every possible game, so a minimax player never loses.

Can I make a GUI version?

Yes, with Tkinter or Pygame. Keep the same winner() function and swap input() for button clicks.

What does lru_cache do in the AI?

It remembers results for board positions it has already scored, so repeated positions aren’t searched again. See functools.lru_cache.