Drawing Graphics
with pygame.draw
Before you load a single image, Pygame lets you draw directly onto the screen with plain shapes. In this module you'll learn RGB color, every function in pygame.draw, and how anti-aliasing and screen updates actually work under the hood.
RGB Colors & the Screen Background
Every color in Pygame is an RGB tuple — three integers from 0 to 255 for red, green and blue. (255,0,0) is pure red, (0,0,0) is black, (255,255,255) is white. You can also pass a 4th value (alpha) on surfaces that support transparency.
Filling the background every frame is done with screen.fill(color) — it must happen before you draw anything else, otherwise your shapes get painted over.
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (220, 20, 60)
GREEN = (52, 211, 153)
BLUE = (79, 158, 255)
NAVY = (5, 9, 26)
screen.fill(NAVY) # clear the frame before drawing anything
Lines, Circles & Rectangles
These three functions cover most of what you'll draw before you start using images. Each one takes the target surface, a color, then shape-specific position arguments.
# pygame.draw.line(surface, color, start_pos, end_pos, width)
pygame.draw.line(screen, WHITE, (50, 50), (300, 50), 4)
# pygame.draw.circle(surface, color, center, radius, width=0)
pygame.draw.circle(screen, GREEN, (400, 300), 40) # filled
pygame.draw.circle(screen, GREEN, (400, 300), 40, 3) # outline only
# pygame.draw.rect(surface, color, rect, width=0, border_radius=0)
pygame.draw.rect(screen, BLUE, (100, 150, 200, 100)) # filled
pygame.draw.rect(screen, BLUE, (100, 150, 200, 100), 2, border_radius=8)
Notice the width parameter: 0 (the default) fills the shape solid; any positive number draws only an outline that many pixels thick. Rectangles take a 4-tuple (x, y, width, height) — or a real pygame.Rect object, which is almost always the better choice once you start moving things.
Ellipses, Polygons & Arcs
For anything that isn't a perfect circle or rectangle, Pygame gives you three more shape functions:
pygame.draw.ellipse(surface, color, rect, width=0) — draws an oval that fits exactly inside the given bounding rectangle. A square rect always produces a circle.pygame.draw.polygon(surface, color, point_list, width=0) — connects a list of (x, y) points in order, closing the shape back to the first point. Great for triangles, arrows, and custom terrain.pygame.draw.arc(surface, color, rect, start_angle, stop_angle, width=1) — draws a curved segment of an ellipse. Angles are in radians, measured counter-clockwise from the 3 o'clock position.pygame.draw function shares the same width convention: 0 = filled, N = outline of N pixels. Consistent across shapes, so learn it once.import math
# Ellipse: fits inside the bounding rect
pygame.draw.ellipse(screen, RED, (150, 400, 180, 90))
# Polygon: a triangle from 3 points
pygame.draw.polygon(screen, GREEN, [(500, 400), (560, 500), (440, 500)])
# Arc: half a circle, from 0 to pi radians
pygame.draw.arc(screen, BLUE, (600, 380, 120, 120), 0, math.pi, 4)
Anti-Aliasing & Updating the Screen
Standard pygame.draw shapes have jagged, "pixel-stepped" edges — this is normal for a fast, low-level 2D library. For smoother lines and circles, Pygame provides anti-aliased variants in pygame.gfxdraw (an extra module you import separately), which blend edge pixels with the background color instead of leaving them hard-edged.
| Function | Edges | Speed | When to use |
|---|---|---|---|
pygame.draw.circle | Jagged | Fastest | Gameplay objects, hitboxes, prototypes |
pygame.gfxdraw.aacircle | Smooth | Slightly slower | Menus, logos, polished UI |
Finally, nothing you draw appears on screen until you call pygame.display.flip() (redraws the whole screen) or pygame.display.update() (can redraw just specific rects for performance). This must happen once per frame, at the very end of your game loop, after every draw call.
screen.fill() → 2) all your pygame.draw calls → 3) pygame.display.flip(). Get this order wrong and you'll see flicker or nothing at all.Put It All Together
Here's a complete mini-scene combining every shape from this module into one drawing pass — a simple "sun over hills" scene using only pygame.draw calls:
def draw_scene(screen):
screen.fill((135, 206, 235)) # sky
pygame.draw.circle(screen, (255, 221, 87), (700, 90), 50) # sun
pygame.draw.ellipse(screen, (34, 139, 34), (-50, 380, 500, 260))
pygame.draw.ellipse(screen, (34, 139, 34), (300, 400, 500, 260))
pygame.draw.rect(screen, (139, 69, 19), (370, 300, 60, 100)) # tree trunk
pygame.draw.polygon(screen, (0, 100, 0), [(400, 180), (460, 300), (340, 300)])
Pygame's Coordinate System
Every position you pass to pygame.draw is measured from the top-left corner of the window — that's (0, 0). X increases as you move right, and — unlike math class — Y increases as you move down, not up. Keeping this flipped Y-axis in mind is what makes "gravity pulls down" mean "increase y" once you start writing movement code.
(0, 0) sits at the top-left. X grows rightward, Y grows downward — so a point further down the screen has a larger y-value, not a smaller one.🧠 Quick Knowledge Check
width=0 to a pygame.draw function do?