Quick answer: Run network/file IO on a threading.Thread with a result queue. Main thread polls the queue between event pumps; window stays responsive.

A puzzle game contacts a leaderboard server on submit. Window freezes for several seconds; OS marks “Not Responding”. urllib.request blocks the event loop.

Thread + Queue

import threading
import queue
import urllib.request

result_q = queue.Queue()

def fetch_async(url):
    def worker():
        try:
            data = urllib.request.urlopen(url, timeout=5).read()
            result_q.put(("ok", data))
        except Exception as e:
            result_q.put(("err", e))
    threading.Thread(target=worker, daemon=True).start()

# in main loop
while not result_q.empty():
    status, payload = result_q.get_nowait()
    handle_response(status, payload)

Worker runs in background; main loop drains results. UI stays smooth throughout.

Async Variant

import asyncio

async def async_main():
    while running:
        for event in pygame.event.get(): ...
        await asyncio.sleep(0)   # yield to async tasks

Pygame works with asyncio loops; queue HTTP via aiohttp without blocking.

Web Build Note

Pygame-CE on web (pygbag) uses asyncio natively. All IO must be await-based; sync IO blocks the browser.

Verifying

Submit leaderboard score. Spinner UI animates during request. Result appears when network completes. No “Not Responding”.

“Main thread = render thread = event pump. Don’t starve it with sync IO.”

For local saves too, use threads if the file is large or on slow disk — a save can take 100ms+ and stall the frame.