Real-Time WebP Converter in Python for developers

Kamil Pasik

Real-time WebP converter running in a terminal

Modern sites live and die by performance. Images are often the heaviest assets on a page; moving them to an efficient format is a quick, high-ROI win. This post shows how to watch a folder in real time and convert every new image to WebP automatically-ideal for content pipelines, marketing drops, or design hand-offs.

You’ll get:

  • a tiny Python watcher that reacts instantly to new files
  • sensible default quality settings
  • clear tuning guidance (lossy vs. lossless, alpha, metadata)
  • notes on running it as a background service

Prerequisites

  • Python 3.10+ (examples use 3.11)
  • WebP tools (cwebp binary)
  • Watchdog Python package

macOS (Homebrew)

brew install webp

Debian/Ubuntu

sudo apt-get update && sudo apt-get install -y webp

Fedora

sudo dnf install libwebp-tools

Windows (winget or Chocolatey)

winget install WebP
# or
choco install webp

Verify

cwebp -version

Install Watchdog

python -m pip install watchdog

Optional: pin dependencies

# requirements.txt
watchdog>=4.0.0

Minimal real-time converter

Watches the current directory, converts new or moved images to WebP using high-quality, multi-threaded settings, and preserves ICC color profiles.

#!/usr/bin/env python3.11
import time, subprocess, os, sys
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler

# Sensible defaults
CWEBP_PARAMS = ['-m', '6', '-q', '85', '-mt', '-af', '-metadata', 'icc']

class ImageHandler(PatternMatchingEventHandler):
    def __init__(self):
        patterns = ["*.jpg", "*.jpeg", "*.png", "*.tif", "*.tiff",
                    "*.JPG", "*.JPEG", "*.PNG", "*.TIF", "*.TIFF"]
        super().__init__(patterns=patterns, ignore_directories=True)

    def on_created(self, event):
        self._convert(event.src_path)

    def on_moved(self, event):
        self._convert(event.dest_path)

    def _convert(self, src):
        base, _ = os.path.splitext(src)
        dst = base + '.webp'
        if os.path.exists(dst) and os.path.getmtime(dst) >= os.path.getmtime(src):
            print(f"Skip (up-to-date): {dst}")
            return
        print(f"Converting {src}{dst}")
        try:
            subprocess.run(['cwebp', *CWEBP_PARAMS, src, '-o', dst], check=True)
        except FileNotFoundError:
            print("ERROR: 'cwebp' not found on PATH. Install WebP tools.", file=sys.stderr)
        except subprocess.CalledProcessError as e:
            print(f"cwebp failed: {e}", file=sys.stderr)

if __name__ == "__main__":
    watch_dir = os.path.abspath("./")
    observer = Observer()
    handler = ImageHandler()
    observer.schedule(handler, watch_dir, recursive=False)
    print(f"Watching {watch_dir} … (Ctrl+C to stop)")
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

How it works

  • Observer hooks into OS file events.
  • PatternMatchingEventHandler filters only image extensions (case-insensitive).
  • On create or move, we run cwebp with a good default quality/CPU profile.
  • If a .webp already exists and is newer, we skip to save time.

  • -m 6 - compression method (0–6). Higher = more CPU, better compression.
  • -q 85 - quality (0–100). Sweet spot for photos; try 75–82 for smaller files.
  • -mt - multi-threading.
  • -af - auto-filter, better compression on noisy images.
  • -metadata icc - keep ICC profile so colors stay true.

Optional

  • -alpha_q 70–90 - control alpha quality for transparent PNGs.
  • -lossless - for graphics/line art where you need pixel-perfect output. Combine with -z 9 for max effort.

Rules of thumb

  • Photos: lossy -q 80–86.
  • UI/line art: try -lossless -z 9 (often beats PNG), or lossy with higher -q.
  • Transparent PNGs: add -alpha_q (70–90) to preserve edges without huge files.

CLI version (folder, recursive, lossy/lossless)

#!/usr/bin/env python3.11
import time, subprocess, os, sys, argparse, shutil
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler

def build_params(lossless: bool, quality: int, alpha_q: int|None):
    if lossless:
        params = ['-lossless', '-z', '9', '-mt', '-metadata', 'icc']
    else:
        params = ['-m', '6', '-q', str(quality), '-mt', '-af', '-metadata', 'icc']
        if alpha_q is not None:
            params += ['-alpha_q', str(alpha_q)]
    return params

class ImageHandler(PatternMatchingEventHandler):
    def __init__(self, params):
        patterns = ["*.jpg","*.jpeg","*.png","*.tif","*.tiff",
                    "*.JPG","*.JPEG","*.PNG","*.TIF","*.TIFF"]
        super().__init__(patterns=patterns, ignore_directories=True)
        self.params = params

    def on_created(self, event):
        self._convert(event.src_path)

    def on_moved(self, event):
        self._convert(event.dest_path)

    def _convert(self, src):
        base, _ = os.path.splitext(src)
        dst = base + '.webp'
        if os.path.exists(dst) and os.path.getmtime(dst) >= os.path.getmtime(src):
            print(f"Skip (up-to-date): {dst}")
            return
        print(f"Converting {src}{dst}")
        subprocess.run(['cwebp', *self.params, src, '-o', dst], check=False)

def main():
    if not shutil.which('cwebp'):
        print("ERROR: 'cwebp' not found on PATH. Install WebP tools.", file=sys.stderr)
        sys.exit(1)

    ap = argparse.ArgumentParser(description="Real-time WebP converter")
    ap.add_argument('folder', nargs='?', default='.', help='Folder to watch (default: current)')
    ap.add_argument('--recursive', action='store_true', help='Watch subfolders')
    ap.add_argument('--lossless', action='store_true', help='Use lossless WebP')
    ap.add_argument('--quality', '-q', type=int, default=85, help='Lossy quality 0..100 (default: 85)')
    ap.add_argument('--alpha-q', type=int, default=None, help='Alpha channel quality 0..100')
    args = ap.parse_args()

    params = build_params(args.lossless, args.quality, args.alpha_q)

    watch_dir = os.path.abspath(args.folder)
    handler = ImageHandler(params)
    observer = Observer()
    observer.schedule(handler, watch_dir, recursive=args.recursive)
    print(f"Watching {watch_dir} (recursive={args.recursive}) … Ctrl+C to stop")
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

if __name__ == "__main__":
    main()

Examples

# Lossy, quality 82, recursive
python watch_webp.py ./images --recursive -q 82

# Lossless for UI assets
python watch_webp.py ./assets/ui --recursive --lossless

Folder strategy & integration tips

  • Don’t overwrite originals. Output to .webp next to the source, or write to a sibling directory (e.g. dist-webp/) if your pipeline expects a separate path.
  • Ignore existing WebPs. The script reacts only to new files; it won’t touch existing .webp.
  • Naming collisions. image.png -> image.webp. If a conflicting name already exists, use a separate output dir.
  • CDN “Accept” conversion. Many CDNs auto-convert to WebP for supported browsers. This local step is still useful for static exports, previews, or tighter control over quality.

Run as a background service

macOS (launchd)

# webpwatch.sh
#!/usr/bin/env zsh
cd /path/to/project
python3 watch_webp.py ./images --recursive -q 85

Create ~/Library/LaunchAgents/com.yourname.webpwatch.plist, then:

launchctl load ~/Library/LaunchAgents/com.yourname.webpwatch.plist

Linux (systemd)

# /etc/systemd/system/webpwatch.service
[Unit]
Description=Real-time WebP watcher

[Service]
WorkingDirectory=/path/to/project
ExecStart=/usr/bin/python3 /path/to/watch_webp.py ./images --recursive -q 85
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now webpwatch

Windows (Task Scheduler)

python.exe C:\path_to\watch_webp.py C:\path_to\images --recursive -q 85

Quality tuning cheat-sheet

  • Start at -q 85 for photos; drop to 82 or 78 if files are still large and artifacts acceptable.
  • UI/Icons: try --lossless -z 9 (often smaller than PNG) or lossy with -q 90+.
  • Transparency: tune -alpha_q (70–90) for crisp edges.
  • Big batches: -m 6 compresses best; if you need speed, try -m 4.

Troubleshooting

  • “cwebp not found” - ensure it’s installed and on PATH (cwebp -version).
  • Nothing happens - confirm you’re dropping files into the watched folder; some editors save to temp and rename-this handler listens to both create and move.
  • Double conversions - the “skip if newer” guard prevents rework.
  • Color shifts - keep -metadata icc so the ICC profile survives.

Wrap-up

You now have a low-friction, real-time image optimization step to plug into any workflow. It’s small, predictable, and easy to operate-exactly the kind of engineering that saves money without drama.