Quick answer: Pre-scale assets to display size before shipping. Use one shared atlas with subsurfaces for individual frames. PNG vs JPEG is a disk concern; in memory both are RGBA = 4 bytes/pixel.
Here is how to fix Pygame memory ballooning when you load 4K PNG sprites. The decompressed RGBA buffer is the cost; pre-scale to actual display size.
The Symptom
Game loads. Memory jumps from 50 MB to 800 MB after sprites load. Some platforms run out of memory.
What Causes This
Decompressed size dominates. 4096x4096 RGBA = 64 MB. Twenty such sprites = 1.3 GB.
No atlasing. Each sprite is its own image; per-image overhead adds up.
The Fix
Step 1: Pre-scale assets. If display size is 256x256, ship at 256x256. Use Photoshop, ImageMagick, or a build script to resize:
# ImageMagick
convert big.png -resize 256x256 small.png
Step 2: Use a single atlas.
atlas = pygame.image.load("atlas.png").convert_alpha()
def get_frame(x, y, w, h):
return atlas.subsurface((x, y, w, h))
player_idle = get_frame(0, 0, 64, 64)
player_run = get_frame(64, 0, 64, 64)
subsurface shares memory with the parent surface; near-zero per-frame cost.
Step 3: Free unused surfaces. Python GC should clear them when no references remain. Confirm with Surface(0,0) replacements.
Step 4: Profile with tracemalloc.
import tracemalloc
tracemalloc.start()
# load assets
snap = tracemalloc.take_snapshot()
for stat in snap.statistics("lineno")[:10]:
print(stat)
Find which loads dominate.
Step 5: For pixel-perfect art, scale at runtime via blit. Render at 1x then scale-up the screen via SCALED display flag. Source assets stay small.
“Pre-scale to display size. One atlas with subsurfaces. Free what you do not need. Memory stays sane.”
Related Issues
For convert_alpha basics, see convert_alpha. For event queue, see Event Queue.
Pre-scale. Atlas + subsurface. Profile. Memory under control.