WebP is now supported in every major browser and is typically 25-35% smaller than PNG at visually identical quality. The conversion itself is one line in Pillow. The interesting parts are quality tuning, alpha channel handling, and batch processing without exhausting memory on huge image sets.

Method 1: Pillow (the canonical Python image library)

Pillow is the de facto standard for image work in Python. It supports WebP encoding natively as long as libwebp is present (it ships with the Pillow wheels on PyPI).

pip install Pillow
from PIL import Image

def png_to_webp(in_path: str, out_path: str, quality: int = 85, lossless: bool = False) -> None:
    with Image.open(in_path) as img:
        save_kwargs = {"quality": quality, "method": 6}
        if lossless:
            save_kwargs["lossless"] = True
            save_kwargs.pop("quality", None)
        img.save(out_path, "WEBP", **save_kwargs)

png_to_webp("hero.png", "hero.webp")
png_to_webp("diagram.png", "diagram.webp", lossless=True)

Three knobs that matter:

  • quality (1-100, default 80) — sweet spot for photos is 75-85. Below 60 you start seeing chroma artifacts.
  • method (0-6, default 4) — encoding effort vs. speed. method=6 is slowest but gives the best compression ratio. For one-off conversions, always use 6.
  • lossless=True — for diagrams, screenshots, or any image with sharp edges. Quality is ignored when lossless is on.

Method 2: Wand (ImageMagick bindings)

If you already use ImageMagick on the command line, Wand exposes the same engine in Python. Useful when you need ImageMagick-specific features like advanced color profiles or HEIC input.

apt install libmagickwand-dev
pip install Wand
from wand.image import Image

def png_to_webp(in_path: str, out_path: str, quality: int = 85) -> None:
    with Image(filename=in_path) as img:
        img.format = "webp"
        img.compression_quality = quality
        img.save(filename=out_path)

png_to_webp("hero.png", "hero.webp")

Wand is a thin layer over ImageMagick — same quality settings, same edge cases. Use it when you want to mix WebP work with operations Pillow can't easily do (color management, perceptual hashing, complex compositing).

Method 3: ChangeThisFile API (no local libs, handles weird inputs)

If you're processing PNGs uploaded by users — different bit depths, unusual color spaces, occasional corruption — the API absorbs the variability. Get a free API key (no card required, 100 conversions/month).

import requests

API_KEY = "ctf_sk_your_key_here"

def png_to_webp(in_path: str, out_path: str) -> None:
    with open(in_path, "rb") as f:
        response = requests.post(
            "https://changethisfile.com/v1/convert",
            headers={"Authorization": f"Bearer {API_KEY}"},
            files={"file": f},
            data={"source": "png", "target": "webp"},
            timeout=60,
        )
    response.raise_for_status()
    with open(out_path, "wb") as out:
        out.write(response.content)

png_to_webp("user_upload.png", "compressed.webp")

The API handles 16-bit PNGs, animated PNGs (returned as still or animated WebP), and any color space ImageMagick understands. No need to install libwebp, MagickWand, or worry about manylinux compatibility.

When to use each

ApproachBest forTradeoff
PillowPure-Python projects, predictable PNG inputs, full control over qualityManual batch scripting
Wand / ImageMagickMixed format pipelines, color profile workNative dep (libmagickwand) on every machine
ChangeThisFile APIUser uploads, edge runtimes, no local image libsPer-call cost, network call

CLI alternative: cwebp

If you don't need Python at all, the upstream cwebp binary ships with Google's libwebp distribution and is the fastest option for one-off conversions.

cwebp -q 85 -m 6 hero.png -o hero.webp
cwebp -lossless diagram.png -o diagram.webp

# Batch with find:
find . -name "*.png" -exec sh -c 'cwebp -q 85 "$1" -o "${1%.png}.webp"' _ {} \;

cwebp uses the same encoder as Pillow under the hood, so quality output is identical for the same settings. The CLI is the right choice for ad-hoc work; Python is the right choice when WebP conversion is part of a larger pipeline.

Batch processing in production

For directories of thousands of images, parallelize with concurrent.futures. Pillow releases the GIL during image encoding, so threads work fine.

from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from PIL import Image

def convert_one(p: Path, out_dir: Path, quality: int = 85) -> None:
    out = out_dir / (p.stem + ".webp")
    with Image.open(p) as img:
        img.save(out, "WEBP", quality=quality, method=6)

in_dir = Path("./png_in")
out_dir = Path("./webp_out")
out_dir.mkdir(exist_ok=True)

pngs = list(in_dir.glob("*.png"))
with ThreadPoolExecutor(max_workers=8) as pool:
    list(pool.map(lambda p: convert_one(p, out_dir), pngs))

method=6 is CPU-heavy. With 8 workers on a typical laptop, you'll convert ~1000 medium PNGs (1920x1080) in about a minute.

Pillow is the default for any Python project doing routine PNG-to-WebP work. Wand if you need ImageMagick features. The API for user uploads where you can't predict what arrives. Free tier is 100 conversions/month, no card.