Quick answer: Use pygame.event.get() and check for KEYDOWN events for discrete actions like jump or fire. Use pygame.key.get_pressed() only for continuous held-key movement. Mixing the two for the same action drops fast presses between frames.
You add a jump button to your Pygame platformer. The player taps it rapidly and half the jumps do not register. You test with longer presses and it works every time. The bug is not random — it is a direct consequence of how Pygame exposes input, and the fix is to pick the right API for each type of action.
The Symptom
- Quick taps on the jump key are ignored.
- Holding movement keys works fine.
- The problem gets worse at lower frame rates.
- Menu navigation fires multiple times per press because you are using
get_pressedwith a boolean flag.
Two APIs, Two Purposes
Pygame has two input APIs that look interchangeable but behave differently.
pygame.event.get() returns every input event that happened since the last call, in order. Each KEYDOWN event fires exactly once when the key is pressed. KEYUP fires exactly once on release. You read events from a queue, so even if the press and release happen within a single frame, both events are still captured and you will see the KEYDOWN on the next frame.
pygame.key.get_pressed() returns the current held state of every key at the exact moment you call it. If the player presses and releases between frames, get_pressed never sees the key down. It is perfect for “is WASD currently held” but wrong for “did the player just press jump.”
Discrete one-shot actions (jump, fire, menu select, pause) belong in event handling. Continuous actions (movement, aiming) belong in get_pressed.
The Canonical Input Loop
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
player = {"x": 400, "y": 500, "vy": 0, "on_ground": True}
running = True
while running:
# 1. Drain and handle events (discrete inputs)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and player["on_ground"]:
player["vy"] = -12
player["on_ground"] = False
elif event.key == pygame.K_ESCAPE:
running = False
# 2. Read held keys (continuous inputs)
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
player["x"] -= 5
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
player["x"] += 5
# 3. Update physics
player["vy"] += 0.6
player["y"] += player["vy"]
if player["y"] >= 500:
player["y"] = 500
player["vy"] = 0
player["on_ground"] = True
# 4. Render
screen.fill((30, 30, 40))
pygame.draw.rect(screen, (200, 100, 100),
(player["x"], player["y"], 32, 32))
pygame.display.flip()
clock.tick(60)
pygame.quit()
The structure is always:
- Drain events for discrete actions.
- Read get_pressed for continuous actions.
- Update game state.
- Render.
Swap 1 and 2 and nothing breaks — both need to happen per frame. Skip either and you get bugs: skip event.get and the OS thinks you hung; skip get_pressed and movement stutters.
Don’t Forget pygame.event.pump
If your code reads only key.get_pressed() and never calls event.get(), the event queue fills up, the OS thinks your game is unresponsive, and get_pressed stops updating. Add pygame.event.pump() in that case — it drains the queue without returning events to you.
The best practice is to always call event.get() even if you ignore the result, because that handles the pump automatically.
Key Repeat
If you want a key press to fire repeatedly while held (common in menus), enable key repeat:
pygame.key.set_repeat(300, 50) # 300ms delay, 50ms interval
With repeat enabled, KEYDOWN events fire once on press and then repeatedly while held. Turn it off (set_repeat() with no args) for gameplay contexts where you do not want repeat behavior.
Verifying the Fix
Tap the jump key as fast as possible for ten seconds. Count how many jumps occurred. With the bug, you will see far fewer jumps than presses. With the fix, every press produces exactly one jump, even rapid taps that happen within a single frame.
“In Pygame, discrete actions live in event.get and continuous actions live in key.get_pressed. Mix them up and you ship a game that misses fast inputs.”
Related Issues
For broader Pygame performance work, see Pygame performance tips for indie developers. For sprite collision issues, see Pygame sprite collision not detected between groups. For text rendering, see Pygame text rendering blurry after scale.
A single jump bug has cost more indie Pygame developers more frustration than any other Pygame pitfall. If you take one thing away: discrete actions go in event.get, not key.get_pressed.