Do a pixel by pixel set inside a loop is, as observed in the question, not efficient enough for game development. Masking is the way to go (assuming our libraries of choice implement this in an efficient way).
How to mask in pygame
When calling Surface.blit() you will use the special_flags parameter to control how pixels are combined. In concrete, most situations will require the pygame.BLEND_RGBA_MULT and an extra image asset to use as the mask image, probably all opaque pixels to white and the rest with alpha set to 0. Procedural generation of the mask image is possible but I found more practical to use an asset.
Also I find more efficient to cache a version of the image with the mask already applied.
BLEND_RGBA_MULT will multiply source pixels with destination surface pixels. This can be used to achieve many funny effects, but the most useful is when your mask image contains all white pixels and those you want to discard with alpha set to 0. Alpha 0 in the mask results in alpha 0. And a white pixel in the mask results in the same color than source image.
Demonstration

To run the following demo you will need the following assets:
avatar1.png 
avatar2.png 
avatar3.png 
mask.png 
mask.png image is invisible with this site background color, but if you move the mouse pointer over it you will see where it is. Right click and save.
The assets were quickly made thanks to opengameart.org. Original authors require attribution so:
- Credit Noble Master Games as follows (linking is optional): "Avatar graphics created by Noble Master Games" and link to http://www.noblemaster.com
- Credit the artist "Liea" as follows (optional): "Avatar graphics designed by Mei-Li Nieuwland" and link to http://liea.deviantart.com
From game: http://www.ageofconquest.com/
And finally, the Python code:
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys, pygame pygame.init() size = width, height = 640, 480 black = 0, 0, 0 screen = pygame.display.set_mode(size) # some globals we need avatar_w = 64 avatar_h = 64 # our game objects are (inneficient) maps with two keys: rect and image avatar1 = dict() avatar2 = dict() avatar3 = dict() avatar1["rect"] = pygame.Rect(16, 16, avatar_w, avatar_h) avatar2["rect"] = pygame.Rect(16 + avatar_w + 8, 16, avatar_w, avatar_h) avatar3["rect"] = pygame.Rect(16 + avatar_w * 2 + 8 + 8, 16, avatar_w, avatar_h) clickables = (avatar1, avatar2, avatar3) # resource loading # crash if the files are not in place (OK for now) avatar1["image"] = pygame.image.load("avatar1.png") avatar2["image"] = pygame.image.load("avatar2.png") avatar3["image"] = pygame.image.load("avatar3.png") selected_avatar = None # procedural generation of a circled mask is possible but for now we # will use an asset mask = pygame.image.load("mask.png") masked_avatar = dict() masked_avatar["image"] = mask masked_avatar["rect"] = pygame.Rect(size[0] / 2 - avatar_w / 2, 16 + avatar_h * 2, avatar_w, avatar_h) renderables = (avatar1, avatar2, avatar3, masked_avatar) def update_masked_avatar (avatar): global masked_avatar # TODO: consider clearing then blit to masked_avatar rather than # cloning mask each time, but it's not so simple as it sounds, when # you blit, you need the alphas from mask to be transferred too. # The lazy, I don't want to do research now, approach is to clone. surf = mask.copy() surf.blit(avatar["image"], (0, 0), None, pygame.BLEND_RGBA_MULT) masked_avatar["image"] = surf # main loop while 1: for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() mouse_pos = pygame.mouse.get_pos() # process the clickables list for obj in clickables: if obj["rect"].collidepoint(mouse_pos): if selected_avatar != obj: selected_avatar = obj update_masked_avatar(obj) screen.fill(black) # process the renderables list for obj in renderables: screen.blit(obj["image"], obj["rect"]) pygame.display.flip()