Understanding Snake — Building the Classic Game in Python
A simple project that quietly teaches you game loops, collision detection, state management, and why rendering performance is harder than it looks.
Introduction
One of the projects I enjoyed building the most was a classic Snake game in Python using the built-in turtle module. Although the project seems simple on the surface, it introduced several important programming concepts: game loops, collision detection, state management, rendering, and object movement.
Full source: github.com/Martynas2016/Snake-Game.
Play it in the browser
The original is Python/turtle and runs on the desktop, so I ported the same rules to HTML Canvas for this post. Click the canvas to focus, then press Space to start. Arrows or WASD to move.
The project is split across four files:
main.py— entry point, game loop, collision checkssnake.py— the snake's body and movementfood.py— the red dot the snake eatsscore.py— the scoreboard
The Game Loop (main.py)
The core of the game is the update loop. On every tick, the game refreshes the screen, processes movement, and checks every collision condition.
I used screen.tracer(0) to disable Turtle's automatic redraw — this lets me batch every per-tick change and push one frame per iteration, which is what makes the game feel like ~10 fps instead of stuttering as each segment moves individually.
from turtle import Screen
from snake import Snake
from food import Food
from score import Score
import time
screen = Screen()
screen.setup(width=600, height=600)
screen.bgcolor("black")
screen.title("Snake")
screen.tracer(0) # Turns off automatic animation
snake = Snake()
food = Food()
score = Score()
screen.listen()
screen.onkey(snake.up, "Up")
screen.onkey(snake.down, "Down")
screen.onkey(snake.left, "Left")
screen.onkey(snake.right, "Right")
# Game loop — ~10 frames per second
game_is_on = True
while game_is_on:
screen.update()
time.sleep(0.1)
snake.move()
# Collision with food
if snake.head.distance(food) < 15:
food.refresh()
snake.extend()
score.increase_score()
# Detect collision with wall
if (snake.head.xcor() > 280 or snake.head.xcor() < -280
or snake.head.ycor() > 280 or snake.head.ycor() < -280):
score.game_over()
game_is_on = False
# Detect collision with tail — if head touches any body segment, game over
for segment in snake.segments[1:]:
if snake.head.distance(segment) < 10:
game_is_on = False
score.game_over()
screen.exitonclick()Snake Movement (snake.py)
One of the more interesting challenges was implementing realistic snake movement. Each segment needs to follow the segment in front of it while keeping consistent spacing.
The trick is to iterate the segments in reverse — moving the tail-most segment first to the position of the one ahead of it, then working forward. If you go in normal order, you overwrite positions before reading them.
from turtle import Turtle
STARTING_POSITION = [(0, 0), (-20, 0), (-40, 0)]
MOVE_DISTANCE = 20
UP = 90
DOWN = 270
LEFT = 180
RIGHT = 0
class Snake:
def __init__(self):
self.segments = []
self.create_snake()
self.head = self.segments[0]
def create_snake(self):
# Create a square segment at each starting position and store it in a list
for position in STARTING_POSITION:
self.add_segment(position)
def add_segment(self, position):
new_segment = Turtle("square")
new_segment.color("white")
new_segment.penup()
new_segment.goto(position)
self.segments.append(new_segment)
def extend(self):
self.add_segment(self.segments[-1].position())
def move(self):
# Walk the segments from tail to head, copying the position ahead.
# Doing this in reverse avoids overwriting positions before reading them.
for seg_num in range(len(self.segments) - 1, 0, -1):
new_x = self.segments[seg_num - 1].xcor()
new_y = self.segments[seg_num - 1].ycor()
self.segments[seg_num].goto(new_x, new_y)
self.head.forward(MOVE_DISTANCE)
def up(self):
if self.head.heading() != DOWN:
self.head.setheading(UP)
def down(self):
if self.head.heading() != UP:
self.head.setheading(DOWN)
def left(self):
if self.head.heading() != RIGHT:
self.head.setheading(LEFT)
def right(self):
if self.head.heading() != LEFT:
self.head.setheading(RIGHT)The direction handlers all share a small but important rule: the snake can't reverse onto itself in a single turn. So up() only changes heading if the snake isn't currently moving down. Without this guard, pressing the opposite arrow key would walk the head straight into the next body segment.
Food (food.py)
The food is a small red circle that respawns at a random location every time the snake eats it. By inheriting from Turtle directly, the Food instance can be passed to snake.head.distance(food) without any wrapper logic.
from turtle import Turtle
import random
class Food(Turtle):
def __init__(self):
super().__init__()
self.shape("circle")
self.penup()
self.shapesize(stretch_len=0.5, stretch_wid=0.5)
self.color("red")
self.speed("fastest")
self.refresh()
def refresh(self):
random_x = random.randint(-280, 280)
random_y = random.randint(-280, 280)
self.goto(random_x, random_y)Scoreboard (score.py)
The scoreboard is its own Turtle subclass that writes text at the top of the screen and re-draws itself whenever the score increases.
from turtle import Turtle
ALIGMENT = "center"
FONT = ("Arial", 24, "normal")
class Score(Turtle):
def __init__(self):
super().__init__()
self.score = 0
self.color("white")
self.penup()
self.goto(0, 270)
self.update_scoreboard()
self.hideturtle()
def update_scoreboard(self):
self.write(f"Score: {self.score}", align=ALIGMENT, font=FONT)
def game_over(self):
self.goto(0, 0)
self.write(f"Game Over", align=ALIGMENT, font=FONT)
def increase_score(self):
self.score += 1
self.clear()
self.update_scoreboard()Collision Detection
All three collision checks live in the main loop:
- Food collision —
snake.head.distance(food) < 15returns true when the head is within 15 pixels of the food. Trigger respawn, grow the snake, increment score. - Wall collision — compare
xcor()andycor()against the playfield boundary (±280, since the window is 600×600 and the segments are 20px squares). - Self collision — loop over
snake.segments[1:](every segment except the head) and check distance against the head. The slice matters; comparing the head against itself would end the game on frame one.
Managing Game State
As the game grew, splitting logic into separate classes made it much easier to reason about:
Snake— body state and directionFood— position and respawnScore— number tracking and displaymain.py— wiring and the loop
Each class has a single responsibility, which made debugging much faster when something went wrong.
What I Learned
This project improved my understanding of game loops, rendering systems, object movement, collision detection, and state management.
It also taught me how smaller programming concepts combine together to create interactive applications — the same patterns scale up to far more complex games and simulations.