Snake is the project I suggest when someone wants a first game they can finish in one afternoon and still understand every line of.
A snake game in Python needs five things: a window, a head that moves on a timer, arrow keys that turn it, food that makes the body grow, and a crash check for the walls and the snake’s own tail. Here’s what we’re building:

In this tutorial, I’ll build that snake game in Python with the turtle module, one step at a time, and you’ll run the file after every step. At the end there’s the same game in Tkinter.
- A 620 x 680 window with a bordered 29 x 29 playing field
- Arrow-key turns that can’t reverse the snake into itself
- Random food, a growing body, score and high score
- Wall and tail collisions, a GAME OVER message and Space to play again
Before You Start: what the Python snake game needs
You don’t install anything. The turtle module ships with Python on Windows and macOS because it sits on top of Tkinter (on some Linux distros you add the python3-tk package). Check yours:
import platform
import tkinter
import turtle
print("Python:", platform.python_version())
print("Tk:", tkinter.Tcl().eval("info patchlevel"))
print("turtle ontimer available:", hasattr(turtle, "ontimer"))
Output:
Python: 3.12.5
Tk: 8.6.13
turtle ontimer available: True

Mine reported Python 3.12.5 and Tk 8.6.13. These are the numbers the whole game is built on, so you can change one value later and the rest follows:
| Setting | Value | Why |
|---|---|---|
STEP | 20 px | One square; every turtle shape is 20 x 20 by default |
LIMIT | 280 | Squares run from -280 to 280, so the field is 29 x 29 |
DELAY | 100 ms | About 10 moves a second |
| Window | 620 x 680 px | Field plus room for the score line |
| Keys | Arrow keys + Space | Turn, and restart after GAME OVER |
If turtle is new to you, my turtle programming in Python guide explains the coordinate system: (0, 0) is the center and y grows upward.
Step 1: Create the snake game window with turtle.Screen()
Create snake.py. The screen holds the settings, the background color and one important switch:
import random
import turtle
STEP = 20 # the snake moves one 20 px square at a time
LIMIT = 280 # squares run from -280 to 280 on both axes
DELAY = 100 # milliseconds between moves
screen = turtle.Screen()
screen.setup(620, 680)
screen.title("Snake Game - Python Turtle")
screen.bgcolor("#1b4332") # dark green
screen.tracer(0) # we decide when to redraw
turtle.done()
screen.tracer(0) turns off turtle’s automatic animation. Without it, the window redraws after every tiny move and a long snake crawls; with it, nothing appears until we call screen.update() once per frame.
turtle.done() keeps the window open and listening. Window sizes and titles are covered in more depth in customizing the Python turtle window.

Checkpoint: run python snake.py. A dark green window titled “Snake Game – Python Turtle” opens and stays open until you close it.
Step 2: Draw the border and the snake head
Next we add a border so players can see the walls, plus the head. Paste this above turtle.done(), then add screen.update() after it:
# a thin border around the playing field
pen = turtle.Turtle(visible=False)
pen.penup()
pen.color("#95d5b2")
pen.pensize(3)
pen.goto(-LIMIT - 12, -LIMIT - 12)
pen.pendown()
for _ in range(4):
pen.forward(2 * LIMIT + 24)
pen.left(90)
pen.penup()
head = turtle.Turtle("square") # 20 x 20 px by default
head.color("#ffd166") # yellow head
head.penup() # move without drawing a line
segments = [] # body squares, used from Step 5
direction = moved = "stop" # nothing moves until a key press
penup() matters on the head: a turtle draws a line wherever it goes, and we want a moving square, not a trail. The border pen draws once and hides itself.
The two state variables at the end, direction and moved, both start as "stop". You’ll see in Step 3 why we need two of them. The colors are hex strings; Python turtle colors lists the names turtle accepts too.

Checkpoint: a light green square frames the field and a small yellow square sits in the middle at (0, 0).
Step 3: Move the snake with the arrow keys
The snake moves on a timer, not in a while True loop. screen.ontimer(game_loop, DELAY) asks Tkinter to call game_loop again in 100 ms, so the window stays responsive between moves.
MOVES = {"up": (0, STEP), "down": (0, -STEP),
"left": (-STEP, 0), "right": (STEP, 0)}
OPPOSITE = {"up": "down", "down": "up", "left": "right", "right": "left"}
def turn(new):
"""Change direction, but never back into the snake's own neck."""
global direction
if new != OPPOSITE.get(moved): # compare with the LAST MOVE made
direction = new
turn() refuses the opposite of the last move made, not the opposite of direction. That one word stops a bug where two quick key presses turn the snake back into its own neck (the troubleshooting section shows it happening).
def move():
global moved
x, y = head.position()
dx, dy = MOVES[direction]
head.goto(x + dx, y + dy)
moved = direction
def game_loop():
if direction != "stop":
move()
screen.update() # draw the new frame
screen.ontimer(game_loop, DELAY) # run again in 100 ms
screen.listen() # send key presses to this window
screen.onkeypress(lambda: turn("up"), "Up")
screen.onkeypress(lambda: turn("down"), "Down")
screen.onkeypress(lambda: turn("left"), "Left")
screen.onkeypress(lambda: turn("right"), "Right")
game_loop()
turtle.done()
screen.listen() gives the window keyboard focus; forget it and the keys do nothing. Each onkeypress takes a function, so a short lambda passes the direction. Getting input in Python turtle covers the other key and text options.

Checkpoint: the head moves one square every tick. In my run it ended at (100, 80): five steps right, four up.
Note: every screenshot here comes from the real game window. A small helper inside the script calls turn() for the demo, so I could reproduce each run exactly.
Step 4: Add food at a random spot
Food is another turtle, this time a red circle. place_food() picks a random square with random positions, as in Python turtle random, and retries if the snake is already on it:
food = turtle.Turtle("circle")
food.color("#ef476f") # red food
food.penup()
score = 0
def place_food():
"""Put the food on a random square the snake isn't using."""
taken = {(round(t.xcor()), round(t.ycor())) for t in [head] + segments}
cells = range(-LIMIT, LIMIT + 1, STEP)
while True:
spot = (random.choice(cells), random.choice(cells))
if spot not in taken:
food.goto(spot)
return
The set comprehension collects every square the snake uses. Turtle positions are floats like 100.00, so we round them before comparing; the in operator on a set is then an instant lookup.
Now the loop checks whether the head landed on the food. head.distance(food) is the straight-line distance between the two turtles, and anything under half a square counts:
def game_loop():
global score
if direction != "stop":
move()
if head.distance(food) < STEP / 2: # the head is on the food
score += 1
place_food()
screen.update()
screen.ontimer(game_loop, DELAY)
place_food()
game_loop()
turtle.done()

Checkpoint: each time the head reaches the red dot, the dot jumps somewhere else. In my run the score variable reached 3 with no body yet.
Step 5: Grow the snake body when it eats
The body is a Python list of square turtles. On every move, each segment steps into the spot of the one in front of it, starting from the tail, and the first segment takes the head’s old spot.
def add_segment(spot):
seg = turtle.Turtle("square")
seg.color("#52b788") # lighter green body
seg.penup()
seg.goto(spot)
segments.append(seg)
def move():
"""Shift the body forward, step the head, return the old tail spot."""
global moved
tail = segments[-1].position() if segments else head.position()
for i in range(len(segments) - 1, 0, -1):
segments[i].goto(segments[i - 1].position())
if segments:
segments[0].goto(head.position())
x, y = head.position()
dx, dy = MOVES[direction]
head.goto(x + dx, y + dy)
moved = direction
return tail
Why from the tail? If you moved segment 1 first, segment 2 would copy segment 1’s new position and the body would collapse onto itself. The Python for loop with range(len(segments) - 1, 0, -1) counts backward for exactly that reason.
move() now returns where the tail was before the move. When the snake eats, a new segment goes exactly there, so the body grows from the back:
def game_loop():
global score
if direction != "stop":
tail = move()
if head.distance(food) < STEP / 2: