Quick answer: pygame.key.set_repeat(delay, interval) only generates synthetic KEYDOWN events when the event subsystem is initialized, the video display exists, KEYDOWN is not in the blocked event list, and the window has focus. Call it after display.set_mode(), audit your set_blocked list, and never mix it with key.get_pressed() in the same input path.
You are building a text input field or a menu cursor that should auto-repeat when the player holds an arrow key. You read the docs, find pygame.key.set_repeat(400, 50), drop it into your setup, and run the game. The first KEYDOWN fires when you press the key. Then nothing. The key sits down, the player waits, and the cursor stays put. Or it works in your prototype but breaks the moment you refactor your initialization order. Key repeat in pygame has a surprising number of preconditions, and missing any one of them produces silent failure.
The Symptom
You expect held keys to generate repeated KEYDOWN events at the interval you set. Instead one of these happens:
One KEYDOWN, then silence. The initial press fires correctly. No repeats are generated regardless of how long the key is held. Releasing and re-pressing fires another single event.
Repeats fire too fast or too slow. Events arrive but the cadence does not match the delay/interval you passed. Often the cadence matches the OS keyboard repeat rate from system settings instead.
Repeats stop after focus loss. The player Alt-Tabs out and back. The key is still physically down, but pygame is no longer firing repeats. Releasing and pressing again works.
Movement double-fires. Your character moves two tiles per press because both your key.get_pressed() polling and your KEYDOWN handler are reading the same input.
What Causes This
Init order. pygame.key.set_repeat() hooks into the SDL event subsystem, which only becomes fully functional after the video subsystem creates a window. If you call set_repeat() before display.set_mode(), the call quietly does nothing on Windows and macOS. On Linux the behavior depends on the SDL version. The fix is trivial — reorder the calls — but the silent failure makes it easy to chase the wrong cause for hours.
Event filtering. A common pattern in performance-sensitive games is to call pygame.event.set_blocked([MOUSEMOTION, KEYDOWN, KEYUP]) to skip events you do not need. If KEYDOWN is in that list, SDL still generates repeat events but pygame discards them before they reach your event loop. set_repeat() appears to do nothing because the events never arrive at your handler.
Focus loss. SDL pauses input event generation when the window does not have focus. This includes synthetic repeat events. When focus returns, pygame does not know whether a key is still held — it has missed any KEYUP events that occurred outside the window. The repeat machinery only restarts on the next physical KEYDOWN.
Mixing polling with events. pygame.key.get_pressed() reads the current key state directly from SDL each time you call it. It returns True every frame the key is held, regardless of set_repeat(). If your movement code polls get_pressed() while your menu code listens for KEYDOWN repeats, the two paths can both fire on the same frame, causing double-input. Worse, the OS-level keyboard repeat (which most desktop OSes also generate) can mix with pygame’s synthetic repeats to produce inconsistent timing.
The Fix
Step 1: Set up in the right order. Window first, then repeat. Always.
import pygame
pygame.init()
# Display MUST come before set_repeat
screen = pygame.display.set_mode((1280, 720))
pygame.display.set_caption("My Game")
# 400 ms delay before first repeat, 50 ms between
pygame.key.set_repeat(400, 50)
# Sanity check — should print (400, 50)
print("Repeat:", pygame.key.get_repeat())
pygame.key.get_repeat() returns the active delay and interval. If it returns (0, 0) after you set non-zero values, the call did not take — almost always because the display was not ready. Add the print as a sanity check during development; remove it in release builds.
Step 2: Audit your event filter. Search your codebase for set_blocked and set_allowed. If you are using either, make sure KEYDOWN is allowed.
# Block only what you really do not need
pygame.event.set_blocked([
pygame.MOUSEMOTION,
pygame.WINDOWENTER,
pygame.WINDOWLEAVE,
])
# Explicitly allow the keyboard events we depend on
pygame.event.set_allowed([
pygame.KEYDOWN,
pygame.KEYUP,
pygame.QUIT,
])
# Verify in your debug build
assert not pygame.event.get_blocked(pygame.KEYDOWN), \
"KEYDOWN is blocked — repeats will be discarded"
The assert guard catches the case where a teammate adds KEYDOWN to a blocklist later and breaks repeats without realizing it.
Pick One Input Style
The most subtle bug in pygame input code is mixing get_pressed() and KEYDOWN/KEYUP events for the same action. They give you different information: get_pressed() tells you what is held right now, events tell you what changed and (with set_repeat()) what should fire on a cadence. Use the right tool for each job:
Use events for discrete actions. Menu navigation, text input, jump (which fires once per press), interact, pause. These should listen for KEYDOWN and ignore the held state. set_repeat() makes sense here for menus and text input.
Use polling for continuous actions. Movement, aiming, charge attacks, anything where you need to know “is this key still down?” every frame. For these, call get_pressed() in your update loop and ignore KEYDOWN entirely.
If you want repeats for one path and polling for another in the same game, that is fine — pygame can deliver both — but make sure your event handler does not run polling logic and your polling code does not read events. Separate update functions per system keeps this clean.
Handling Focus Loss
When the player Alt-Tabs out, SDL stops emitting KEYDOWN repeats. Most games do not care — gameplay should pause when the window loses focus anyway — but if you have a tool window or a background scrolling text feed that should keep going, you need to handle the focus return.
Listen for pygame.WINDOWFOCUSGAINED in your event loop. When it fires, call pygame.event.pump() to flush any queued state changes, then check key.get_pressed() for the keys you care about. If a key is still held, post a synthetic KEYDOWN yourself so your repeat-driven UI picks up where it left off. Do not rely on SDL to detect the still-held key — it does not, by design, because there is no safe way to tell whether the user pressed the key while the window was unfocused.
Tuning Delay and Interval
The defaults of (400, 50) are reasonable for menus but feel sluggish for fast text editing and frantic for action-game cursor movement. A few starting points: text input fields use (500, 30) — long initial delay, fast repeat. Tile-based movement cursors use (200, 100). Number sliders that scrub continuously use (300, 16) for one tick per frame at 60 fps. Always test with both keyboard and held-down virtual keys (some accessibility software emulates holds with sticky keys, which interact strangely with repeat timing).
“If
set_repeat()looks broken, the bug is almost never inset_repeat(). It is in the order things were initialized, the events you blocked, or the polling code you forgot you wrote.”
Related Issues
If your modifier keys (Shift, Ctrl, Alt) appear stuck in the down position even after release, see Pygame Key Modifier State Stuck. If your input feels laggy regardless of repeat settings, your event loop may be processing too late in the frame — pump events at the top of each tick, before any rendering.
Display first, set_repeat second — reverse that order and you get silence forever.