Quick answer: pygame.sprite.collide_circle uses sprite.radius if it exists, otherwise it guesses from the rect. Set self.radius explicitly for predictable circular hits.

Bullet-vs-asteroid collision with collide_circle feels off — hits register too early or too late. Neither sprite has a radius attribute, so Pygame is estimating.

How collide_circle Picks a Radius

If a sprite has a radius attribute, collide_circle uses it. If not, it derives a radius from the rect — roughly half the diagonal — which is usually bigger than the visible shape.

Set radius Explicitly

class Asteroid(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = ...
        self.rect = self.image.get_rect()
        self.radius = 28   # tuned to the visible rock

Now collide_circle uses 28 — consistent and tuned to the art, not the bounding box.

collide_circle_ratio

For a quick global adjustment without per-sprite radii, collide_circle_ratio(0.75) scales the rect-derived radius by a factor. Good for prototyping; explicit radii are better for shipping.

Keep radius in Sync with Scale

If you scale a sprite at runtime, update radius too — it doesn’t auto-track the image size.

Verifying

Fire bullets at asteroids. Hits register when the circles visually overlap — not a rect-corner early, not a pixel late. Scaled asteroids collide correctly.

“collide_circle wants a radius. Set it explicitly and tune it to the art.”

Draw the collision circle in a debug overlay while tuning — the right radius is obvious in two seconds visually.