Skip to main content

How to Convert Between JPG and PNG in Python

Web developers frequently need to convert between JPG (small file size, no transparency) and PNG (lossless quality, transparency support). While the conversion seems simple, handling alpha channels correctly is essential to avoid errors and visual artifacts.

This guide covers proper conversion techniques using the Pillow library.

Basic Setup

pip install Pillow
from PIL import Image

JPG to PNG (Simple)

Converting JPG to PNG is straightforward since JPG has no transparency to preserve:

from PIL import Image

def jpg_to_png(source_path, dest_path):
"""Convert JPG to PNG format."""
img = Image.open(source_path)
img.save(dest_path, 'PNG')

# Usage
jpg_to_png("photo.jpg", "photo.png")

With Optimization

from PIL import Image

def jpg_to_png_optimized(source_path, dest_path, compress_level=6):
"""
Convert JPG to PNG with compression.

Args:
compress_level: 0-9, higher = smaller file but slower
"""
img = Image.open(source_path)
img.save(dest_path, 'PNG', optimize=True, compress_level=compress_level)

jpg_to_png_optimized("photo.jpg", "photo.png")

PNG to JPG (Handling Transparency)

PNG files use RGBA mode (with alpha channel for transparency). JPG only supports RGB, so direct conversion fails:

from PIL import Image

# ⛔️ This crashes on transparent PNGs!
img = Image.open("logo_transparent.png")
img.save("logo.jpg") # OSError: cannot write mode RGBA as JPEG
Common Error

OSError: cannot write mode RGBA as JPEG occurs when saving a PNG with transparency as JPG. You must handle the alpha channel explicitly.

from PIL import Image

def png_to_jpg(source_path, dest_path, background_color=(255, 255, 255), quality=95):
"""
Convert PNG to JPG with proper transparency handling.

Args:
background_color: RGB tuple for transparent areas (default: white)
quality: JPG quality 1-100
"""
img = Image.open(source_path)

if img.mode in ('RGBA', 'LA', 'P'):
# Create background canvas
background = Image.new('RGB', img.size, background_color)

# Handle palette mode with transparency
if img.mode == 'P':
img = img.convert('RGBA')

# Paste using alpha channel as mask
if img.mode == 'RGBA':
background.paste(img, mask=img.split()[3])
else:
background.paste(img, mask=img.split()[1]) # LA mode

background.save(dest_path, 'JPEG', quality=quality)
else:
# No transparency, simple conversion
img.convert('RGB').save(dest_path, 'JPEG', quality=quality)

# Usage
png_to_jpg("logo_transparent.png", "logo.jpg") # White background
png_to_jpg("icon.png", "icon.jpg", background_color=(0, 0, 0)) # Black background

Simple Conversion (Transparency Becomes Black)

from PIL import Image

def png_to_jpg_simple(source_path, dest_path, quality=95):
"""
Simple PNG to JPG conversion.
Warning: Transparent areas become BLACK.
"""
img = Image.open(source_path)
rgb_img = img.convert('RGB')
rgb_img.save(dest_path, 'JPEG', quality=quality)

# Only use when transparency isn't present or black background is acceptable
png_to_jpg_simple("photo.png", "photo.jpg")

Checking Image Mode Before Conversion

from PIL import Image

def analyze_image(path):
"""Check image properties before conversion."""
img = Image.open(path)

info = {
'path': path,
'format': img.format,
'mode': img.mode,
'size': img.size,
'has_transparency': img.mode in ('RGBA', 'LA', 'P'),
}

# Check if palette mode has transparency
if img.mode == 'P':
info['has_transparency'] = 'transparency' in img.info

return info

# Usage
print(analyze_image("unknown.png"))
# {'path': 'unknown.png', 'format': 'PNG', 'mode': 'RGBA', 'size': (800, 600), 'has_transparency': True}

Batch Conversion

Convert All PNGs to JPGs

from PIL import Image
from pathlib import Path

def batch_png_to_jpg(input_dir, output_dir=None, background=(255, 255, 255)):
"""Convert all PNGs in a directory to JPGs."""
input_path = Path(input_dir)
output_path = Path(output_dir) if output_dir else input_path
output_path.mkdir(parents=True, exist_ok=True)

converted = 0

for png_file in input_path.glob("*.png"):
try:
img = Image.open(png_file)

if img.mode in ('RGBA', 'LA', 'P'):
if img.mode == 'P':
img = img.convert('RGBA')

bg = Image.new('RGB', img.size, background)
if img.mode == 'RGBA':
bg.paste(img, mask=img.split()[3])
else:
bg.paste(img, mask=img.split()[1])
img = bg
else:
img = img.convert('RGB')

output_file = output_path / f"{png_file.stem}.jpg"
img.save(output_file, 'JPEG', quality=90)
converted += 1

except Exception as e:
print(f"Error converting {png_file.name}: {e}")

print(f"Converted {converted} files")

# Usage
batch_png_to_jpg("images/", "converted/")

Convert All JPGs to PNGs

from PIL import Image
from pathlib import Path

def batch_jpg_to_png(input_dir, output_dir=None):
"""Convert all JPGs in a directory to PNGs."""
input_path = Path(input_dir)
output_path = Path(output_dir) if output_dir else input_path
output_path.mkdir(parents=True, exist_ok=True)

for jpg_file in input_path.glob("*.jpg"):
img = Image.open(jpg_file)
output_file = output_path / f"{jpg_file.stem}.png"
img.save(output_file, 'PNG', optimize=True)

# Also handle .jpeg extension
for jpeg_file in input_path.glob("*.jpeg"):
img = Image.open(jpeg_file)
output_file = output_path / f"{jpeg_file.stem}.png"
img.save(output_file, 'PNG', optimize=True)

batch_jpg_to_png("photos/", "png_output/")

Preserving Metadata

from PIL import Image
from PIL.ExifTags import TAGS

def convert_preserving_metadata(source, dest, target_format):
"""Convert image while preserving EXIF metadata."""
img = Image.open(source)

# Extract EXIF data
exif = img.info.get('exif')

# Handle conversion
if target_format.upper() == 'JPEG' and img.mode in ('RGBA', 'P'):
if img.mode == 'P':
img = img.convert('RGBA')
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
elif target_format.upper() == 'JPEG':
img = img.convert('RGB')

# Save with EXIF if available
if exif:
img.save(dest, target_format, exif=exif, quality=95)
else:
img.save(dest, target_format, quality=95)

# Usage
convert_preserving_metadata("photo.png", "photo.jpg", "JPEG")

Resizing During Conversion

from PIL import Image

def convert_and_resize(source, dest, max_size=(1920, 1080), background=(255, 255, 255)):
"""Convert format and resize in one operation."""
img = Image.open(source)

# Resize maintaining aspect ratio
img.thumbnail(max_size, Image.Resampling.LANCZOS)

# Determine output format from extension
is_jpg = dest.lower().endswith(('.jpg', '.jpeg'))

if is_jpg:
if img.mode in ('RGBA', 'LA', 'P'):
if img.mode == 'P':
img = img.convert('RGBA')
bg = Image.new('RGB', img.size, background)
if img.mode == 'RGBA':
bg.paste(img, mask=img.split()[3])
else:
bg.paste(img, mask=img.split()[1])
img = bg
else:
img = img.convert('RGB')
img.save(dest, 'JPEG', quality=90)
else:
img.save(dest, 'PNG', optimize=True)

# Usage
convert_and_resize("large_logo.png", "thumbnail.jpg", max_size=(200, 200))

Format Comparison

FeatureJPGPNG
TransparencyNoYes (alpha channel)
CompressionLossyLossless
File SizeSmallerLarger
Best ForPhotos, gradientsLogos, text, screenshots
Color Depth24-bitUp to 48-bit

Summary

ConversionMethodKey Consideration
JPG → PNGimg.save("out.png")Simple, automatic
PNG → JPGComposite onto backgroundRequired for transparency
PNG → JPG (simple).convert("RGB")Transparency becomes black
Best Practice

When converting PNG to JPG, always use the background composite method unless you're certain the PNG has no transparency. A simple .convert("RGB") turns transparent pixels black, which is rarely the desired result for logos and graphics.