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.