Pygame Basics: Windows, Loops, and Drawing

A canvas and a clock

Advanced 14 min

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 .venv then activate it: source .venv/bin/activate on macOS and Linux, .venv\Scripts\activate on 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.

Example · bouncing_box.py
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()
Output
# 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.
A complete Pygame program. Run it locally with: python bouncing_box.py

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.

Example
# 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}")
Output
frame 1: x=195 speed=5
frame 2: x=195 speed=-5
frame 3: x=190 speed=-5
frame 4: x=185 speed=-5
The bounce, four frames of it, with no window in sight.

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)

Common mistake: Forgetting to handle the quit event

Why it happens:

Closing a window feels like something the operating system should handle.

How to fix it:

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

Why it happens:

The program runs fine without it — extremely fine.

How to fix it:

clock.tick(60) caps the loop. Without it, speed depends on how fast the machine is.

Common mistake: Drawing once, outside the loop

Why it happens:

Drawing feels like setup, done once like creating the window.

How to fix it:

Every frame is drawn from scratch. Drawing belongs inside the loop, after the state updates.

Common mistake: Forgetting to clear the screen each frame

Why it happens:

The first frame looks perfect.

How to fix it:

Without screen.fill() every frame stays on top of the last, and moving shapes smear.

Why does drawing happen inside the loop?

What does clock.tick(60) control?

Which event closes the window?

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)

What to learn next

You built a real window: pip install pygame-ce, the three-part loop of handle events, update state and draw, pygame.QUIT so the window can close, screen.fill() so shapes do not smear, and clock.tick(60) so the game runs at the same speed everywhere. The bounce logic you checked without a window at all.

A moving square is not yet a game. Movement, Collisions, and Scores adds keyboard control and the four comparisons behind every collision.