Pygame Basics: Windows, Loops, and Drawing
A canvas and a clock
In this lesson
Time for something you can see. Pygame gives Python a window to draw in and a clock to keep time. Everything else is the loop from two lessons ago.
Pygame needs a real screen, so this is the first unit where the code runs on your own machine rather than on this page. The setup is two commands and worth doing: watching a rectangle you control move across a window is a different kind of satisfying from printing text.
Explain it like I’m 5
Pygame gives Python a canvas and a clock, so your program can draw pictures over and over fast enough to look like movement.
Getting it running
Pygame is a package, installed with pip exactly as in Unit 5. Use a virtual environment so it stays out of your system Python.
Two commands from a terminal, in a folder you have made for this:
python -m venv .venvthen activate it:source .venv/bin/activateon macOS and Linux,.venv\Scripts\activateon Windows.pip install pygame-ce
pygame-ce is the community edition, the actively maintained fork, and what most people should install today. Everything is imported as pygame either way, so all Pygame tutorials still apply.
The three-part loop
Every Pygame program has the same body, and it is the event loop with the parts named: handle events, update state, draw.
The one line beginners always leave out is the quit event. Without it the window has no way to close, and you end up killing the terminal.
import pygame
WIDTH, HEIGHT = 400, 300
BACKGROUND = (20, 24, 40)
BOX_COLOR = (43, 124, 211)
FPS = 60
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("First window")
clock = pygame.time.Clock()
x, speed = 0, 3
running = True
while running:
# 1. Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 2. Update state
x += speed
if x <= 0 or x >= WIDTH - 50:
speed = -speed
# 3. Draw
screen.fill(BACKGROUND)
pygame.draw.rect(screen, BOX_COLOR, (x, 125, 50, 50))
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
# A 400x300 window opens with a blue square sliding # left and right, bouncing off both edges at 60 FPS. # Closing the window ends the program cleanly.
The update half, without the window
Notice that section 2 of that loop, the movement and the bounce, mentions Pygame nowhere. It is arithmetic on two numbers, and it can be written and checked entirely on its own.
That is the separation this unit keeps returning to, and here is the practical payoff: the trickiest part of the program is the part you can test without ever opening a window.
# The update half of a game loop, with no Pygame involved.
WIDTH = 200
def update(x, speed):
"""Move, then bounce off either edge."""
x += speed
if x <= 0 or x >= WIDTH:
speed = -speed
x += speed
return x, speed
x, speed = 190, 5
for frame in range(4):
x, speed = update(x, speed)
print(f"frame {frame + 1}: x={x} speed={speed}")
frame 1: x=195 speed=5 frame 2: x=195 speed=-5 frame 3: x=190 speed=-5 frame 4: x=185 speed=-5
Run the bounce logic yourself. Complete update so it moves the box by speed, and reverses direction when it reaches either edge, remembering to step back after flipping so it does not get stuck on the wall. Start at 96 in a 100-wide world so the bounce happens immediately.
WIDTH = 100
def update(x, speed):
# TODO: move by speed, then bounce off either edge
return x, speed
x, speed = 96, 6
positions = []
for _ in range(3):
x, speed = update(x, speed)
positions.append(x)
print("positions:", positions)
print("final speed:", speed)
Three steps: x += speed, then if x <= 0 or x >= WIDTH: flip with speed = -speed and apply the new speed with another x += speed. The first frame moves to 102, bounces back to 96, and the speed is negative from then on.
WIDTH = 100
def update(x, speed):
x += speed
if x <= 0 or x >= WIDTH:
speed = -speed
x += speed
return x, speed
x, speed = 96, 6
positions = []
for _ in range(3):
x, speed = update(x, speed)
positions.append(x)
print("positions:", positions)
print("final speed:", speed)
positions: [96, 90, 84]
final speed: -6
Common mistake: Forgetting to handle the quit event
Closing a window feels like something the operating system should handle.
Check for pygame.QUIT in the event loop and set your running flag to False, or the window cannot be closed.
Common mistake: Leaving out the clock
The program runs fine without it — extremely fine.
clock.tick(60) caps the loop. Without it, speed depends on how fast the machine is.
Common mistake: Drawing once, outside the loop
Drawing feels like setup, done once like creating the window.
Every frame is drawn from scratch. Drawing belongs inside the loop, after the state updates.
Common mistake: Forgetting to clear the screen each frame
The first frame looks perfect.
Without screen.fill() every frame stays on top of the last, and moving shapes smear.
Why does drawing happen inside the loop?
Clear, draw the current state, reveal, once per pass of the loop.
What does clock.tick(60) control?
It caps the frame rate so the game runs at the same speed on any machine.
Which event closes the window?
pygame.QUIT arrives when the close button is pressed; your loop has to act on it.
Mini exercise (medium)
Extend the movement logic to two dimensions, as a bouncing ball needs. Write update(x, y, dx, dy) that moves in both directions and bounces off all four walls of a 100×50 area, then run it for five frames and print the path.
Give it a shot. Complete the code and press Run; there’s nothing to download or configure.
WIDTH, HEIGHT = 100, 50
def update(x, y, dx, dy):
# TODO: move in both directions
# TODO: bounce off the left/right walls, and off the top/bottom
return x, y, dx, dy
x, y, dx, dy = 94, 44, 6, 4
path = []
for _ in range(5):
x, y, dx, dy = update(x, y, dx, dy)
path.append((x, y))
print("path:", path)
print("speeds:", dx, dy)
It is the one-dimensional bounce done twice, independently. Handle x against WIDTH and y against HEIGHT in separate if statements. A ball can hit the side wall without hitting the floor.
WIDTH, HEIGHT = 100, 50
def update(x, y, dx, dy):
x += dx
if x <= 0 or x >= WIDTH:
dx = -dx
x += dx
y += dy
if y <= 0 or y >= HEIGHT:
dy = -dy
y += dy
return x, y, dx, dy
x, y, dx, dy = 94, 44, 6, 4
path = []
for _ in range(5):
x, y, dx, dy = update(x, y, dx, dy)
path.append((x, y))
print("path:", path)
print("speeds:", dx, dy)
path: [(94, 48), (88, 48), (82, 44), (76, 40), (70, 36)]
speeds: -6 -4
assert update(50, 25, 5, 5) == (55, 30, 5, 5), "away from the walls it should just move"
nx, ny, ndx, ndy = update(98, 25, 5, 5)
assert ndx == -5 and ndy == 5, "hitting the right wall reverses x only"
nx, ny, ndx, ndy = update(50, 48, 5, 5)
assert ndy == -5 and ndx == 5, "hitting the floor reverses y only"
print("✓ Looks good!")