Quick answer: When animation frames vary in size, preserve a stable anchor like rect.midbottom across the swap — not topleft. Or trim/pad frames to a uniform size.

A character’s attack animation has a wider frame; with each swap the sprite visibly jitters because the new rect’s top-left is the previous one but the contents shifted.

Anchor by Feet, Not by Corner

old_anchor = self.rect.midbottom
self.image = anim.current_frame()
self.rect = self.image.get_rect(midbottom=old_anchor)

Using midbottom keeps the character’s feet planted regardless of frame size. For a UI sprite, center; for a thrown item, midtop; etc. Pick the anchor that matches the visual intent.

Uniform Frame Size

If you control the spritesheet, pad every frame to the same size. The anchor question disappears — rect dimensions never change. Slightly more memory, a lot less code to think about.

Don't Mix .image.fill With Animations

Filling a fresh-loaded image surface with alpha 0 to “normalize” can corrupt subsequent frames if they share data. Always work on a copy or a freshly-loaded surface.

Verifying

Play the attack animation — the character stays anchored, the wider frames extend outward from the body instead of teleporting the whole sprite.

“Frame size changes ↔ anchor matters. Preserve midbottom, not topleft.”

Uniform-size frames are still the simplest answer — saves anchor logic and makes hitbox math consistent too.