Short answer

ImageOps.invert(img) for RGB, L and 1-bit images. For RGBA, invert the RGB channels and put the alpha back unchanged (code below). Convert palette (P) and CMYK images to RGB first.

A negative

invert.py
from PIL import Image, ImageOps

img = Image.open("photo.jpg")
negative = ImageOps.invert(img)
negative.save("negative.jpg")
print(img.getpixel((690, 600)), "->", negative.getpixel((690, 600)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(79, 55, 11) -> (176, 200, 244)
The sunset photo as a negative: a blue-white sky with dark red clouds and white silhouettes on a pale ground
Real output: ImageOps.invert() on the sunset photo.

Which modes ImageOps.invert accepts

which_modes.py
from PIL import Image, ImageOps

photo = Image.open("photo.jpg")
for mode in ["RGB", "L", "1", "RGBA", "LA", "P", "CMYK", "I;16", "F"]:
    img = photo.convert("I").convert("I;16") if mode == "I;16" else photo.convert(mode)
    try:
        ImageOps.invert(img)
        print(f"{mode:<5} ok")
    except (OSError, NotImplementedError) as e:
        print(f"{mode:<5} {type(e).__name__}: {e}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
RGB   ok
L     ok
1     ok
RGBA  OSError: not supported for mode RGBA
LA    OSError: not supported for mode LA
P     NotImplementedError: mode P support coming soon
CMYK  OSError: not supported for mode CMYK
I;16  OSError: not supported for mode I;16
F     OSError: not supported for mode F

Only 8-bit RGB, greyscale and 1-bit images work directly. The two messages people search for most are not supported for mode RGBA (transparent PNGs) and mode P support coming soon (GIFs and palette PNGs).

Transparent images (RGBA) without losing the alpha

invert_rgba.py
from PIL import Image, ImageChops, ImageOps

logo = Image.open("logo.png")                       # RGBA, transparent corners

# Invert colour channels only, keep alpha
r, g, b, a = logo.split()
rgb = ImageOps.invert(Image.merge("RGB", (r, g, b)))
inverted = Image.merge("RGBA", (*rgb.split(), a))
inverted.save("logo_inverted.png")

# The tempting alternative inverts the alpha channel too
everything = ImageChops.invert(logo)
print("corner alpha  original:", logo.getpixel((2, 2))[3], " kept:", inverted.getpixel((2, 2))[3],
      " ImageChops.invert:", everything.getpixel((2, 2))[3])
print("centre alpha  original:", logo.getpixel((450, 180))[3], " kept:", inverted.getpixel((450, 180))[3],
      " ImageChops.invert:", everything.getpixel((450, 180))[3])
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
corner alpha  original: 0  kept: 0  ImageChops.invert: 255
centre alpha  original: 255  kept: 255  ImageChops.invert: 0
The logo with inverted colours: an orange rounded rectangle with a black circle and black lettering, corners still transparent
Real output: colours inverted, transparency kept.

ImageChops.invert() accepts RGBA without complaint, and that is the trap: it inverts all four channels, so transparent areas become opaque and opaque areas vanish. Split the channels as above, or with NumPy: a[..., :3] = 255 - a[..., :3].

Palette, CMYK and 16-bit images

invert_other_modes.py
import numpy as np
from PIL import Image, ImageOps

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

p = photo.quantize(64)                                         # palette image
print("P    ->", ImageOps.invert(p.convert("RGB")).mode)       # convert, then invert

cmyk = photo.convert("CMYK")
print("CMYK ->", ImageOps.invert(cmyk.convert("RGB")).mode)

a16 = np.asarray(photo.convert("L"), dtype=np.uint16) * 257    # 16-bit greyscale data
inv16 = Image.fromarray(65535 - a16)                           # max is 65535, not 255
print("I;16 ->", inv16.mode, inv16.getextrema())
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
P    -> RGB
CMYK -> RGB
I;16 -> I;16 (0, 65535)

Inverting a CMYK image's channels does not produce a photographic negative (the black channel behaves differently), so going through RGB is the right approach anyway. For 16-bit data, subtract from 65535; 255 - a on a uint16 array silently wraps around.

Inverting a mask

Inverting a greyscale mask swaps which part of an operation applies where, which is handy with composite() and paste():

invert_mask.py
from PIL import Image, ImageDraw, ImageOps

photo = Image.open("photo.jpg").resize((400, 267))
mask = Image.new("L", photo.size, 0)
ImageDraw.Draw(mask).ellipse((120, 30, 280, 190), fill=255)    # white = keep

grey = photo.convert("L").convert("RGB")
inside = Image.composite(photo, grey, mask)                    # colour inside the circle
outside = Image.composite(photo, grey, ImageOps.invert(mask))  # colour outside it

sheet = Image.new("RGB", (810, 267), "white")
sheet.paste(inside, (0, 0))
sheet.paste(outside, (410, 0))
sheet.save("mask.jpg")
Tested on ✓ Linux ✓ Windows ✓ macOS
Left: the photo in greyscale except a colour circle in the middle; right: the photo in colour except a greyscale circle
Real output. Left: the mask. Right: ImageOps.invert(mask).

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.