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.