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.