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]

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!

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

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:
- Depth first search in Python
- and, or and not in Python
- List of lists in Python
- What does enumerate do in Python
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.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn