Quick answer: Sprite.kill() removes from Groups but Python keeps it alive while any reference exists. Audit with gc.get_referrers(); switch peripheral references to weakref.ref().
A bullet-hell shooter spawns thousands of enemies per minute. Memory profile shows constant growth despite calling kill() on dead enemies. After 5 minutes, the game stutters from RAM pressure.
Why kill() Doesn't Free
class Enemy(pygame.sprite.Sprite):
def die(self):
self.kill() # removes from all groups
# but elsewhere:
all_enemies = [] # list of enemies for boss targeting
all_enemies.append(enemy)
# after .kill(), still in all_enemies — alive
Lists, dicts, and any attribute holding the sprite keeps it alive in CPython’s refcount.
Audit with gc.get_referrers
import gc
def audit_sprite(sprite):
refs = gc.get_referrers(sprite)
for r in refs:
print(type(r), id(r))
Run after kill(). Lists what holds the sprite. Common: own enemy list, AI target attributes, particle parent refs.
Fix 1: Explicit Cleanup
def on_enemy_died(self, enemy):
enemy.kill()
self.all_enemies.remove(enemy)
for ally in self.allies:
if ally.target is enemy:
ally.target = None
Explicit but error-prone — easy to miss a reference site.
Fix 2: Weakrefs for Peripheral Refs
import weakref
class Ally(pygame.sprite.Sprite):
def set_target(self, enemy):
self.target_ref = weakref.ref(enemy)
def update(self):
target = self.target_ref() if self.target_ref else None
if target and target.alive():
self.aim_at(target)
Weakrefs don’t prevent GC. When the enemy dies and is removed from groups, the weakref returns None.
Verifying
Track len(gc.get_objects()) over time. Should stabilize after warmup. Spawn-then-kill 10,000 enemies; memory returns to baseline within seconds of GC running.
“kill() unhooks from Groups; you still own all the references you took. Weakref the optional ones.”
For shmups especially, the spawn/kill cycle is constant — small leaks compound rapidly. weakref discipline saves hours of profiling later.