Quick answer: Pygame Rect objects are mutable and assignment copies the reference. Spawning 20 sprites from the same template rect gives you 20 sprites all sharing one rect. Always use .copy() or .move() to produce a fresh rect for each sprite.

You set up a bullet pool. Every bullet grabs its rect from a template. You fire ten bullets, move the first one, and all ten move together. The bullets are not ghosts — they’re all pointing at the same Rect object because Python assignment shares references.

The Classic Bug

# BAD: all bullets share one rect
template_rect = pygame.Rect(0, 0, 8, 8)
for _ in range(10):
    bullet = Bullet()
    bullet.rect = template_rect  # shared reference!
    bullets.append(bullet)

# GOOD: each bullet gets its own rect
for _ in range(10):
    bullet = Bullet()
    bullet.rect = template_rect.copy()
    bullets.append(bullet)

The second version produces ten independent rects. Moving one doesn’t affect the others.

move vs move_ip

Every Rect method comes in two flavors. The plain name (move, inflate, clamp) returns a new Rect. The _ip suffix (move_ip, inflate_ip, clamp_ip) mutates in place. Use the non-ip version when you want to avoid accidentally sharing state:

new_pos = bullet.rect.move(5, 0)  # new rect
bullet.rect.move_ip(5, 0)       # modifies the existing rect

Sprite Constructor Pitfall

When you write class Bullet(pygame.sprite.Sprite) and accept a rect argument, store a copy:

class Bullet(pygame.sprite.Sprite):
    def __init__(self, position_rect):
        super().__init__()
        self.rect = position_rect.copy()  # not just =

Verifying

Add print(id(sprite.rect)) for each sprite. If the IDs are all the same, you have sharing. If they differ, each sprite has its own.

“Pygame Rect is a mutable object in Python, which means every assignment is a reference share. Copy once at creation and never think about it again.”

Related Issues

For sprite collision problems, see Pygame sprite collision not detected between groups. For event handling issues, see Pygame event queue dropping key presses.

When in doubt, .copy() the rect. The cost is nothing; the debug time it saves is hours.