Short answer

background.paste(overlay, (x, y), overlay), where overlay is an RGBA image: passing it a second time uses its alpha channel as the mask. If the background is RGBA too and may itself be transparent, use background.alpha_composite(overlay, (x, y)) instead.

Overlay a logo on a photo

overlay.py
from PIL import Image

photo = Image.open("photo.jpg")
logo = Image.open("logo.png")                # RGBA: has an alpha channel
logo.thumbnail((300, 300))

photo.paste(logo, (40, 40), logo)            # third argument: the mask (here, the logo's own alpha)
photo.save("with_logo.jpg")
print(logo.mode, logo.size)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
RGBA (300, 180)

The (x, y) position is the top-left corner of the pasted image. The photo is changed in place; paste() returns None, so don't write photo = photo.paste(...).

Why the corners turn black

Without the mask argument, paste() copies the pixels as they are, including the fully transparent ones, whose colour happens to be black:

with_and_without_mask.py
from PIL import Image

logo = Image.open("logo.png")
logo.thumbnail((300, 300))
bg = Image.open("photo.jpg").crop((900, 0, 1260, 220))      # a patch of sky

without = bg.copy()
without.paste(logo, (30, 20))                # no mask: alpha is ignored, transparent pixels become black
with_mask = bg.copy()
with_mask.paste(logo, (30, 20), logo)

sheet = Image.new("RGB", (730, 220), "white")
sheet.paste(without, (0, 0))
sheet.paste(with_mask, (370, 0))
sheet.save("mask_compare.png")
print("corner without mask:", without.getpixel((32, 22)), " with mask:", with_mask.getpixel((32, 22)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
corner without mask: (0, 0, 0)  with mask: (86, 83, 110)
Two copies of the logo on sky: the left one in a black rectangle, the right one with clean rounded corners
Real output. Left: paste(logo, pos). Right: paste(logo, pos, logo).

ValueError: bad transparency mask

The mask must be mode 1, L, LA, RGBA or RGBa. An RGB image has no transparency to use, so it is refused:

overlay.py
from PIL import Image

photo = Image.open("photo.jpg")
logo = Image.open("logo.png").convert("RGB")     # alpha thrown away somewhere earlier
photo.paste(logo, (40, 40), logo)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
Traceback (most recent call last):
  ...
ValueError: bad transparency mask

Usually the alpha was lost earlier, by a convert("RGB"), by saving to JPEG, or because the file never had any. Check img.mode right after opening. Palette images (P, from GIFs and small PNGs) cannot be masks either; convert them to RGBA first:

valid_masks.py
from PIL import Image

photo = Image.open("photo.jpg")

logo = Image.open("logo.png")
print("logo:", logo.mode, "bands:", logo.getbands())
photo.paste(logo, (40, 40), logo.getchannel("A"))    # explicit: just the alpha band (mode L)

gif = Image.open("spinner.gif")                      # palette image: cannot be a mask itself
print("gif:", gif.mode)
gif = gif.convert("RGBA")                            # converts palette transparency (if any) to alpha
photo.paste(gif, (400, 40), gif)
print("ok")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
logo: RGBA bands: ('R', 'G', 'B', 'A')
gif: P
ok

paste() or alpha_composite()?

On an opaque background (a photo), they give the same result. On a background that is itself transparent, they don't:

paste_vs_alpha_composite.py
from PIL import Image

logo = Image.open("logo.png")                        # the circle is 90% opaque (alpha 230)
canvas = Image.new("RGBA", logo.size, (0, 0, 0, 0))  # fully transparent canvas

pasted = canvas.copy()
pasted.paste(logo, (0, 0), logo)
composited = canvas.copy()
composited.alpha_composite(logo)                     # in-place; Image.alpha_composite(a, b) returns a new image

print("logo pixel:      ", logo.getpixel((170, 180)))
print("paste with mask: ", pasted.getpixel((170, 180)))
print("alpha_composite: ", composited.getpixel((170, 180)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
logo pixel:       (255, 255, 255, 230)
paste with mask:  (230, 230, 230, 207)
alpha_composite:  (255, 255, 255, 230)

paste() blends every channel with the mask, including alpha, so a 90% opaque pixel pasted onto a transparent canvas comes out less opaque than it was, and its colour darker. alpha_composite() does proper "over" compositing. Use it for stickers, layers and anything you will save as PNG with transparency. It needs both images in RGBA mode.

Watermark with opacity, in the corner

watermark.py
from PIL import Image

photo = Image.open("photo.jpg").convert("RGBA")
mark = Image.open("logo.png")
mark.thumbnail((photo.width // 5, photo.width // 5))     # 20% of the photo's width

opacity = 0.5
alpha = mark.getchannel("A").point(lambda a: round(a * opacity))
mark.putalpha(alpha)

margin = 30
pos = (photo.width - mark.width - margin, photo.height - mark.height - margin)   # bottom-right
photo.alpha_composite(mark, pos)
photo.convert("RGB").save("watermarked.jpg", quality=90)
print("watermark", mark.size, "at", pos)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
watermark (320, 192) at (1250, 845)
The sunset photo with a half-transparent blue logo in the bottom-right corner
Real output: logo at 20% width and 50% opacity, 30 px from the corner.

Scaling the existing alpha (rather than setting it to a constant with putalpha(128)) keeps the transparent corners transparent. The watermark tool generates this code with text or tiled marks.

The other errors you may hit

common_errors.py
from PIL import Image

photo = Image.open("photo.jpg")                  # RGB, 1600 x 1067
logo = Image.open("logo.png")                    # RGBA, 600 x 360

tests = {
    "alpha_composite(RGB, RGBA)": lambda: Image.alpha_composite(photo, logo),
    "alpha_composite(sizes differ)": lambda: Image.alpha_composite(photo.convert("RGBA"), logo),
    "paste(mask of wrong size)": lambda: photo.copy().paste(logo, (0, 0), Image.new("L", (10, 10))),
    "blend(sizes differ)": lambda: Image.blend(photo, logo.convert("RGB"), 0.5),
}
for name, fn in tests.items():
    try:
        fn()
        print(f"{name:<31} ok")
    except ValueError as e:
        print(f"{name:<31} ValueError: {e}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
alpha_composite(RGB, RGBA)      ValueError: image has wrong mode
alpha_composite(sizes differ)   ValueError: images do not match
paste(mask of wrong size)       ValueError: images do not match
blend(sizes differ)             ValueError: images do not match

alpha_composite needs two RGBA images of the same size; the method form (a.alpha_composite(b, dest)) lifts the size restriction by taking a position. blend and composite need the same size and mode. paste accepts any position, but its mask must match the pasted image's size.

The outputs and images on this page are the real results of running every example with Pillow 12.3.0 on Python 3.13 (Linux) on 2026-10-15. Every example is also re-run automatically on macOS (Darwin 25.6.0, Pillow 12.3.0), Linux (Linux 6.17.0-1022-azure, Pillow 12.3.0), Windows (Windows 2025Server, Pillow 12.3.0); the line under each example shows the result per operating system. The DejaVu Sans font files sit next to each script so font examples behave the same everywhere. Sample photos: NASA/Sam Lott, public domain. Spotted something wrong? Tell us.