Skip to content

PyGame Support

PyScript includes experimental support for PyGame Community Edition, a Python library for building games. PyGame-CE runs in the browser through PyScript, letting you share games via URL without requiring players to install Python or any dependencies.

This guide explains how to use PyGame-CE with PyScript, covering the differences from traditional PyGame development and techniques for making games work well in the browser.

Warning

PyGame-CE support is experimental. Behaviour may change based on community feedback and bug reports. Please share your experiences via Discord or GitHub to help improve this feature.

Quick start

Create a PyGame-CE application by using the py-game script type:

<script type="py-game" src="my_game.py"></script>

PyGame-CE loads automatically - no pip installation needed. Your game runs in the browser and can be shared via URL like any other web page.

Refer to PyGame-CE's documentation for game development techniques. Most features work in the browser, though some may behave differently due to the browser environment.

Browser considerations

PyGame-CE in the browser differs from local development in several key ways. Understanding these differences helps you write games that work well in both environments.

The browser needs regular opportunities to update the canvas displaying your game. Replace clock.tick(fps) with await asyncio.sleep(1/fps) to give the browser time to render. Better timing techniques exist and are covered later in this guide.

Media files like images and sounds must be explicitly loaded using PyScript's configuration system. Use the files section in your configuration to make assets available.

Python and PyGame-CE versions in the browser may lag behind the latest releases. Check the browser console when PyGame-CE starts to see which versions are available. Functions marked "since 2.5" in the documentation won't work if version 2.4.1 is bundled.

Complete example

Here's a complete bouncing ball game demonstrating PyGame-CE in the browser:

View the complete source code.

This example shows the essential pattern for PyGame-CE in PyScript. The game uses await asyncio.sleep(1/60) to yield control to the browser for canvas updates.

The Python code:

The bouncing ball example from PyGame-CE.
import asyncio
import sys
import pygame

pygame.init()

size = width, height = 320, 240
speed = [2, 2]
black = 0, 0, 0

screen = pygame.display.set_mode(size)
ball = pygame.image.load("intro_ball.gif")
ballrect = ball.get_rect()

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

    ballrect = ballrect.move(speed)
    if ballrect.left < 0 or ballrect.right > width:
        speed[0] = -speed[0]
    if ballrect.top < 0 or ballrect.bottom > height:
        speed[1] = -speed[1]

    screen.fill(black)
    screen.blit(ball, ballrect)
    pygame.display.flip()
    await asyncio.sleep(1/60)

The only addition to standard PyGame code is await asyncio.sleep(1/60), which gives the browser time to render. The await at the top level works in PyScript's async context but won't run locally without modification (covered below).

The HTML file needs a canvas element and the script tag:

Provide a <canvas> element for the game to render.
<!DOCTYPE html>
<html lang="en">
<head>
  <title>PyScript PyGame-CE Example</title>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="stylesheet" 
    href="https://pyscript.net/releases/2026.1.1/core.css">
  <script type="module" 
    src="https://pyscript.net/releases/2026.1.1/core.js"></script>
</head>
<body>
  <canvas id="canvas" style="image-rendering: pixelated"></canvas>
  <script type="py-game" src="quickstart.py" 
    config="pyscript.toml"></script>
</body>
</html>

Info

The style="image-rendering: pixelated" preserves the pixelated look on high-DPI screens. Remove it for smoothed rendering.

The configuration file lists game assets:

[files]
"intro_ball.gif" = ""

Download intro_ball.gif from the example on this website.

Running locally and in browser

The top-level await in the example isn't valid in standard Python (it should be inside an async function). PyScript provides an async context automatically, but local Python doesn't.

Wrap your game in an async function and use a try-except block to detect the environment:

Browser/non-browser compatibility.
import asyncio
import sys
import pygame


async def run_game():
    """
    Main game function.
    """
    pygame.init()

    # Initialise game state...
    size = width, height = 320, 240
    screen = pygame.display.set_mode(size)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        # Game logic...

        await asyncio.sleep(1/60)


try:
    # Check if we're in an async context (PyScript).
    asyncio.get_running_loop()
    asyncio.create_task(run_game())
except RuntimeError:
    # No async context (local Python).
    asyncio.run(run_game())

This pattern works in both environments. PyScript uses create_task(), local Python uses asyncio.run(). Now you can develop locally and publish to the web without changing code.

Info

The pygame.QUIT event never fires in the browser version, but handling it is mandatory for local execution where closing the window generates this event.

Precise frame timing

The await asyncio.sleep(1/60) approach approximates 60 FPS but isn't precise. Frame rendering takes time, so sleeping 1/60th of a second results in actual FPS below 60.

Better timing synchronises with the display refresh rate using requestAnimationFrame in the browser and vsync=1 locally. This requires separating setup from the game loop:

Precise timing.
import sys
import pygame

pygame.init()

size = width, height = 320, 240
speed = pygame.Vector2(150, 150)
black = 0, 0, 0

screen = pygame.display.set_mode(size, vsync=1)
ball = pygame.image.load("intro_ball.gif")
ballrect = ball.get_rect()
clock = pygame.time.Clock()


def run_one_frame():
    """
    Execute one frame of the game.
    """
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

    # Delta time for frame-rate independence.
    dt = clock.tick(300) / 1000

    ballrect.move_ip(speed * dt)

    # Bounce logic...
    if ballrect.left < 0 or ballrect.right > width:
        speed.x = -speed.x
    if ballrect.top < 0 or ballrect.bottom > height:
        speed.y = -speed.y

    screen.fill(black)
    screen.blit(ball, ballrect)
    pygame.display.flip()


# Browser: use requestAnimationFrame.
try:
    from pyscript import window, ffi

    def on_animation_frame(timestamp):
        """
        Called by browser for each frame.
        """
        run_one_frame()
        window.requestAnimationFrame(raf_proxy)

    raf_proxy = ffi.create_proxy(on_animation_frame)
    on_animation_frame(0)

except ImportError:
    # Local: use while loop with vsync.
    while True:
        run_one_frame()

This synchronises with display refresh (usually 60Hz, but can be higher). Delta time (dt) accounts for frame rate variations between machines. Speed is now in pixels per second, multiplied by dt to get movement per frame.

The vsync=1 parameter makes flip() block until the display updates locally. In the browser, vsync=1 does nothing - instead, requestAnimationFrame controls timing.

Note that variable frame rates can cause physics issues. The ball might get stuck in walls if frame skipping occurs. For beginners, the simpler asyncio.sleep method may be easier despite being less precise.

How PyGame-CE integration works

The py-game script type bootstraps Pyodide with PyGame-CE pre-installed. Unlike regular scripts, PyGame-CE always runs on the main thread and cannot use workers.

The target attribute specifies which canvas element displays the game. If omitted, PyScript assumes a <canvas id="canvas"> element exists.

Configuration through the config attribute adds packages or files like images and sounds. This is currently the only configuration PyGame-CE scripts support.

Info

The input() function works in PyGame-CE but uses the browser's native prompt() dialog. Since PyGame-CE runs on the main thread, this is the only way to block for user input. PyScript handles this automatically when you call input().

Experimental status

PyGame-CE support is experimental but functional. You can load pygame-ce manually through regular PyScript if needed, but the py-game script type simplifies multi-game pages and ensures game logic runs on the main thread where PyGame-CE expects it.

Future PyScript versions may include other game engines alongside PyGame-CE. We welcome feedback, suggestions, and bug reports to improve this feature.

What's next

Now that you understand PyGame-CE support, explore these related topics:

Terminal - Use the alternative REPL-style interface for interactive Python sessions.

Editor - Create interactive Python coding environments in web pages with the built-in code editor.

PyScript in JavaScript - drive PyScript from the world JavaScript.

Plugins - Understand the plugin system, lifecycle hooks, and how to write plugins that integrate with PyScript.