Real-Time WebP Converter in Python for developers

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
ormove
, we runcwebp
with a good default quality/CPU profile. - If a
.webp
already exists and is newer, we skip to save time.
Recommended cwebp
settings
-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.