Quick answer: vsync=1 only takes effect with the pygame.SCALED flag (or a hardware-accelerated surface). Pass pygame.SCALED alongside vsync=1.
A game passes vsync=1 to set_mode but the frame rate still runs unlimited and the screen tears. vsync was silently ignored.
vsync Needs SCALED
screen = pygame.display.set_mode(
(1280, 720),
pygame.SCALED, # required for vsync to apply
vsync=1)
On a plain software surface, the vsync request is dropped. pygame.SCALED routes through an accelerated path where vsync is honored.
Confirm It Worked
set_mode doesn’t raise if vsync can’t be enabled — it just falls back. Sanity-check by measuring frame rate: with vsync on it should sit at the monitor’s refresh rate (e.g. 60).
Still Call clock.tick
Even with vsync, call clock.tick(target_fps). vsync paces presentation; clock.tick gives you delta time and a fallback cap if vsync isn’t available on the user’s machine.
Fullscreen + SCALED
SCALED also handles resolution scaling and letterboxing in fullscreen — it pairs naturally with vsync for a clean, tear-free fullscreen mode.
Verifying
With SCALED + vsync, frame rate locks to the monitor refresh and tearing disappears. On a machine where vsync can’t engage, clock.tick still caps the rate.
“vsync rides on SCALED. Request both, and keep clock.tick as a fallback cap.”
SCALED is the modern default for most Pygame games — it gives you vsync, resolution independence, and clean fullscreen in one flag.