How to Create a Tic-Tac-Toe Game in Python
Build a playable Python tic-tac-toe game with a nine-cell board, input validation, win checks, draws, testing, and optional GUI ideas.
Direct answer: represent the 3×3 board with a nine-item list, alternate X and O in a loop, reject invalid or occupied positions, and check the eight winning lines after every legal move. The complete two-player command-line game below uses only Python’s standard library.
What you will build
This local two-player game uses positions 1–9:
1 | 2 | 3
--+---+--
4 | 5 | 6
--+---+--
7 | 8 | 9
A flat list stores cells at indexes 0–8, so position 1 maps to index 0. A win is any of eight rows, columns, or diagonals; a draw means all cells are filled with no winner.
Complete runnable Python game
Save as tic_tac_toe.py and run python tic_tac_toe.py (or python3).
WINNING_LINES = ((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 show_board(board):
rows = [" | ".join(board[start:start+3]) for start in (0,3,6)]
print("\n" + "\n--+---+--\n".join(rows) + "\n")
def get_move(board, player):
while True:
try:
position = int(input(f"Player {player}, choose a position (1-9): "))
except ValueError:
print("Enter a whole number from 1 to 9."); continue
except EOFError:
print("\nInput ended. Goodbye!"); raise SystemExit
if not 1 <= position <= 9:
print("Choose 1 through 9."); continue
index = position - 1
if board[index] != " ":
print("That position is already occupied."); continue
return index
def has_won(board, player):
return any(all(board[index] == player for index in line) for line in WINNING_LINES)
def is_draw(board):
return " " not in board
def play_game():
board, player = [" "] * 9, "X"
print("Tic-Tac-Toe\nPositions:")
show_board([str(number) for number in range(1,10)])
while True:
show_board(board)
board[get_move(board, player)] = player
if has_won(board, player):
show_board(board); print(f"Player {player} wins!"); return
if is_draw(board):
show_board(board); print("It is a draw."); return
player = "O" if player == "X" else "X"
if __name__ == "__main__":
play_game()
How it works
[" "] * 9creates nine empty cells.show_boardprints three slices.get_moveconvertsinput()text to an integer and retries invalid, out-of-range, or occupied choices without consuming a turn.- The mark is placed, then all eight lines are checked immediately.
- If there is no winner, a full board is a draw; otherwise the turn switches.
Implementation choices and extensions
List or dictionary
The list makes indexing and line checks compact. A dictionary keyed 1–9 can read more like the printed board. Do not mix one-based input with zero-based indexes: subtract one exactly once.
Reusable game logic
has_won and is_draw depend only on state, so a GUI can reuse them while replacing terminal input and output.
Optional computer opponent
import random
legal_moves = [i for i, cell in enumerate(board) if cell == " "]
if legal_moves:
board[random.choice(legal_moves)] = "O"
Check that the legal-moves list is non-empty before random.choice, which raises IndexError for an empty sequence. Minimax is a separate strategy project.
GUI path
Tkinter can provide nine buttons, a turn label, and reset behavior. Button callbacks replace input(); keep the board and rule functions unchanged.
Testing checklist
- Test all three rows, columns, and both diagonals.
- Verify an early win ends immediately.
- Fill the board without a line and verify a draw.
- Try letters, decimals, negative numbers, 0, 10, and occupied cells.
- Confirm invalid input does not change the board or turn.
- Confirm redirected input handles
EOFError.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
ValueError |
Non-numeric input reached int(). |
Keep the try/except ValueError retry. |
| Position 1 changes cell 2 | Index bases were mixed. | Use index = position - 1. |
| Full winning board reports draw | Draw checked first. | Check has_won before is_draw. |
| Extra turn after bad input | Turn switched before validation. | Switch only after a legal move and result checks. |
NameError: WINNING_LINES |
Constant was misspelled or scoped locally. | Define it before has_won at module level. |
| Redirected input stops abruptly | EOF raises EOFError. |
Handle it as shown or provide nine input lines. |
Performance, reliability, and cost
Each move checks a fixed nine-cell board and eight lines, so time and memory stay constant. Validate at the input boundary, keep one winning-line definition, and evaluate the result after every legal move. The game uses only built-in Python features, with no network calls or service cost.
Or skip the browser setup
To capture a hosted game board, demo page, or README image without configuring a headless browser, use ScreenshotNeo. See the API documentation.
# cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
# Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
# Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, newsletter popups, and chat widgets are removed before capture.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the verdict and billing result.
- An MCP server lets Claude, Cursor, and other AI agents call
take_screenshot,get_page_info, andcapture_pdf. - 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account.
FAQ
Can I use a nested 3×3 list?
Yes. It mirrors rows directly; document whichever indexing convention you choose.
Why check wins before draws?
The final move can fill the board and complete a line, so the winner must be reported first.
Which Python version works?
Any current stable Python 3 release supports this standard-library code.
How do I replay?
Wrap play_game() in an outer loop and break when players decline another round.


