Quick answer: Use pygame.display.set_mode((W, H), pygame.SCALED | pygame.RESIZABLE). SCALED delegates scaling to SDL2 with hardware acceleration; RESIZABLE allows window resizing without your code re-creating the surface every event.
Drag the window corner. Game flickers, scrambles, briefly shows a tiny version of itself. Pygame’s legacy resize handling tears every frame across the resize event. SCALED fixes most of it.
The Symptom
Resizable Pygame window shows visual artifacts during drag-resize. Game contents shift, partial-render, or briefly clear. After resize completes, the next full frame renders correctly.
What Causes This
OS resize events invalidate the underlying SDL surface. Pygame in legacy mode requires you to call set_mode on each VIDEORESIZE event to re-create the surface. Frames between the event and the re-creation render to a torn buffer.
Pygame 2 introduced SCALED that decouples the logical surface from the window. The window resizes; the logical surface stays the same; SDL2 scales for display.
The Fix
import pygame
pygame.init()
VIRT_W, VIRT_H = 1280, 720
screen = pygame.display.set_mode(
(VIRT_W, VIRT_H),
pygame.SCALED | pygame.RESIZABLE,
vsync=1
)
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# No need to handle VIDEORESIZE specially with SCALED
screen.fill((30, 30, 35))
# draw at virtual resolution
pygame.draw.circle(screen, (200, 100, 100), (640, 360), 100)
pygame.display.flip()
clock.tick(60)
Game logic operates at 1280×720 always. Window can be resized to any size; SDL handles scaling. Drag-resize is smooth.
Without SCALED (Legacy Path)
For pygame < 2 or where SCALED isn’t available, handle VIDEORESIZE manually:
screen = pygame.display.set_mode((1280, 720), pygame.RESIZABLE)
back = pygame.Surface((1280, 720)) # virtual canvas
while running:
for event in pygame.event.get():
if event.type == pygame.VIDEORESIZE:
screen = pygame.display.set_mode(event.size, pygame.RESIZABLE)
# Draw to back at virtual res
back.fill((30, 30, 35))
# ...
# Scale to current window size
scaled = pygame.transform.smoothscale(back, screen.get_size())
screen.blit(scaled, (0, 0))
pygame.display.flip()
Aspect Ratio
If you don’t want stretching, letterbox to maintain aspect:
w, h = screen.get_size()
target_aspect = VIRT_W / VIRT_H
window_aspect = w / h
if window_aspect > target_aspect:
new_w = int(h * target_aspect); new_h = h
else:
new_w = w; new_h = int(w / target_aspect)
offset = ((w - new_w) // 2, (h - new_h) // 2)
Verifying
Run the game. Drag-resize the window. With SCALED: smooth, no flicker. Without: manual handling but acceptable. Toggle vsync = 1 also helps.
“SCALED + RESIZABLE. Render virtual. SDL handles the rest.”
Related Issues
For Pygame fullscreen surface lost, see fullscreen surface. For tick busy-loop, see tick CPU.
SCALED. Resize feels instant.