Coding the classic Snake game in Python is an absolute rite of passage for anyone learning how to program. It is the perfect beginner project because it teaches you core logic without overwhelming you with complex physics engines. Plus, building a snake game using Python just feels right. To keep things as accessible as possible, we are strictly using Python’s built-in graphics library, so we are completely avoiding pygame. This means you do not need to install any external packages—you can jump straight into the code.
Goal and gameplay of Snake
The game window features a moving snake and a solid red square representing the food. When you press an arrow key, the snake begins moving in that direction. Every time the snake collides with the red square (the food), it grows one segment longer. The objective is to see how massive you can grow the snake without crashing into the outer walls or eating your own tail. If you hit a wall or cross your own body, the screen flashes a game over message.
Game window and design plan
We are building a game window sized exactly at 625 pixels by 625 pixels. We divide this window into 25 rows and 25 columns. That means every single square—or tile—is exactly 25 pixels by 25 pixels. So, our tile size is 25. The math here is simple: 25 x 25 equals 625.
When we draw the snake, we assign it an x and y coordinate. The top-left corner is 0,0, and the bottom-right corner is 625,625. The snake starts precisely 5 tiles to the right and 5 tiles down. Because of this, its starting x and y position is not 5,5; it is 5 x 25, which equals 125,125. Every row and column number gets multiplied by the tile size of 25. I messed this math up on my first try, and my snake spawned completely off the screen, so keep your coordinates straight.
How to create game window with Python?
I use Visual Studio Code for this, but any code editor will work just fine. Create a brand new Python file and name it snake.py.
First, we import a couple of essential libraries:
- tkinter: Used for our graphics. It is Python’s standard graphical user interface library.
- random: Used to spawn the food in random locations every time it gets eaten.
In Python, we define constants using uppercase letters. We have 25 rows and 25 columns. The tile size is 25 pixels. The window width is calculated by multiplying the tile size by the number of rows, and the window height is the tile size multiplied by the number of columns.
For the window setup:
- Open a window using
window = tk.Tk(). - Add a title using
window.title("Snake"). - Prevent the user from resizing it using
window.resizable(False, False). I left this out once, and resizing the window completely broke the game grid. - Keep the window running using
window.mainloop().
Now, you have a locked 625×625 window.
Creating Canvas
With the window ready, we build a canvas to draw on:
- Use
tkinter.Canvas. - Set the master as our window.
- Set the BG (background color) to Black.
- Set the width to window width, and height to window height.
- Pack the canvas into the window using
canvas.pack(). - Call
window.update().
To remove the annoying default borders:
- Set border width to 0.
- Set highlight thickness to 0.
This ensures there is absolutely no dead space between the width and height.
Centering window on screen
By default, the window opens in a random spot on your screen every time you run the code. To fix this and center it properly:
- Grab the window dimensions using
window.winfo_width()andwindow.winfo_height(). - Grab the screen dimensions using
window.winfo_screenwidth()andwindow.winfo_screenheight(). - Calculate the X position:
int(screen_width / 2 - window_width / 2) - Calculate the Y position:
int(screen_height / 2 - window_height / 2)
Then apply it using window.geometry(). The format requires a specific string: width x height + x + y. An f-string is perfect for this: f"{window_width}x{window_height}+{window_x}+{window_y}". Now it pops up dead center every single time.
Tile class and drawing Snake
We build a Tile class to store the x and y positions for both the snake and the food. The class simply takes an x and y coordinate.
When starting the game:
- Create the snake’s head with
snake = Tile(5 * tile_size, 5 * tile_size). - Spawn the food exactly 10 tiles right and 10 tiles down with
food = Tile(10 * tile_size, 10 * tile_size).
To render the snake, define a draw function. Inside this function, declare snake as global.
Use canvas.create_rectangle for the drawing. It requires these values:
- Top-left corner x and y:
snake.x,snake.y - Bottom-right corner x and y:
snake.x + tile_size,snake.y + tile_size - fill color: lime green
To create an actual game loop, use window.after. This calls the draw function repeatedly every 100 milliseconds (1/10th of a second). This gives us a smooth 10 frames per second.
Use the exact same method to draw the food:
canvas.create_rectanglefood.x,food.y,food.x + tile_size,food.y + tile_size- fill color: red
How to move Snake?
To get the snake moving, we define velocity X and velocity Y. Velocity is simply the change in position over time. Initially, both are set to 0, so the snake stays perfectly still.
Add a key listener using window.bind to track keyboard inputs. We listen for a Key release event. When a key is released, it triggers the change direction function. This function takes an e parameter, where e stands for event.
Here is the directional logic:
- Up arrow: velocity X = 0, velocity Y = -1
- Down arrow: velocity X = 0, velocity Y = 1
- Left arrow: velocity X = -1, velocity Y = 0
- Right arrow: velocity X = 1, velocity Y = 0
Keep in mind:
- Moving left is the negative X direction.
- Moving right is the positive X direction.
- Moving up is the negative Y direction.
- Moving down is the positive Y direction.
Move function and clearing frames
We define a move function. Inside, declare snake as global.
We add the velocity X to the snake’s x position. You absolutely must multiply this by the tile size; otherwise, the snake creeps forward by a single pixel instead of a full grid tile. Apply the exact same math to the y position.
Call this move function inside your draw loop. Since draw runs 10 times a second, the snake moves continuously at 10 frames per second.
When you first test this, you will see a massive green streak across the screen. That is because the previous frames are not being cleared. To fix this technical glitch, drop canvas.delete("all") at the top of your draw loop to wipe the slate clean before rendering the next frame.
At this point, two major bugs appear:
- The snake can instantly reverse direction (e.g., going right, then immediately left), crushing its own head. We need to prevent this.
- The snake briefly turns invisible when sliding over the food. This happens because the food is drawn over the snake.
The fixes:
- Draw the food first, then the snake. This layer ordering keeps the snake visible during collision.
- Add logic to block reversing:
- On Up key: ensure
velocity Y != 1 - On Down key: ensure
velocity Y != -1 - On Left key: ensure
velocity X != 1 - On Right key: ensure
velocity X != -1
- On Up key: ensure
Food collision and growing Snake
We create a list named snake_body to store all the tail segments. These segments are Tile objects.
Inside the move function, check for a collision between the snake’s head and the food. The detection is simple: do their x and y coordinates match?
- If
snake.x == food.xandsnake.y == food.y, we have a collision.
When they collide:
- Append a new segment to the body:
snake_body.append(Tile(food.x, food.y)). - Teleport the food to a random new location:
food.x = random.randint(0, columns - 1) * tile_sizefood.y = random.randint(0, rows - 1) * tile_size
randintgenerates a random number between 0 and columns – 1. Since we have 25 tiles, the numbers range from 0 to 24. We then multiply by the tile size.
Add the snake body to the draw function:
- Use a
for tile in snake_body:loop. - Run
canvas.create_rectangle(tile.x, tile.y, tile.x + tile_size, tile.y + tile_size, fill="lime green").
However, the body will just sit there while the head moves away. To make the tail follow the head, use this strategy:
- Move the body starting from the very back segment.
- Make every tile take the position of the tile directly in front of it.
- Use a reverse loop:
for i in range(len(snake_body) - 1, -1, -1). - If
i == 0, this is the first segment right behind the head. Assign it the head’s exact x and y coordinates. - Otherwise, grab
prev_tile = snake_body[i - 1]and assign the current tile’s x and y to matchprev_tile.
Now, when the snake eats the food, the tail grows and perfectly slithers behind the head.
Game over states and tracking score
Create a variable named game_over and set it to False by default.
Inside the change direction function:
- Add
if game_over: return. This completely locks the controls once you lose. - Make sure to define game over, food, snake body, and score as global.
Inside the move function:
- Add
if game_over: returnto instantly freeze the snake upon dying.
Here are the conditions for losing:
- Crashing into the outer walls:
snake.x < 0snake.x >= window_widthsnake.y < 0snake.y >= window_heightIf any of these hit, setgame_over = Trueand return.
- Crashing into its own body:
- Run a
for tile in snake_body:loop. - If
snake.x == tile.x and snake.y == tile.y:, setgame_over = Trueand return.
- Run a
How to display score?
Create a score variable starting at 0. Every time you register a food collision, increment the score by 1.
Render the score directly onto the canvas:
- If game over triggers:
- Use
canvas.create_textat x:window_width / 2, y:window_height / 2. - Font: Arial, font size: 20.
- Text: game over (also display the final score).
- fill: White.
- Use
- While playing normally:
- Use
canvas.create_textat x: 30, y: 20. - Font: Arial, size: 10.
- Text: Use an f-string for the score.
- fill: White.
- Use
Since you are modifying the score variable during gameplay, you must define it as global. I missed this on my first run and Python threw a brutal “local variable score referenced before assignment” error that broke the loop.
