HEIC (High Efficiency Image Container) is the default photo format on iPhones since iOS 11. It produces files roughly half the size of JPG at the same visual quality, but adoption outside Apple's ecosystem is patchy. Most ML pipelines, image processing libraries, and web upload flows expect JPG. So you convert.

Python has solid options. The trick is picking one that handles EXIF metadata (orientation matters — without it, half your photos come out sideways) and runs cleanly on Linux servers (HEIC needs libheif, which is not always pre-installed).

Method 1: ChangeThisFile API (zero dependencies)

If you don't want to install libheif system-wide on your servers, hit the API. Get a free API key for 1,000 conversions/month.

import requests

API_KEY = "sk_test_your_key_here"

def heic_to_jpg(heic_path: str, jpg_path: str) -> None:
    with open(heic_path, "rb") as f:
        response = requests.post(
            "https://changethisfile.com/v1/convert",
            headers={"Authorization": f"Bearer {API_KEY}"},
            files={"file": f},
            data={"source": "heic", "target": "jpg"},
            timeout=30,
        )
    response.raise_for_status()
    with open(jpg_path, "wb") as out:
        out.write(response.content)

heic_to_jpg("IMG_1234.HEIC", "IMG_1234.jpg")

For an iPhone backup folder, parallelize:

import os
from concurrent.futures import ThreadPoolExecutor

backup_dir = "iphone_backup"
out_dir = "converted"
os.makedirs(out_dir, exist_ok=True)

heic_files = [f for f in os.listdir(backup_dir) if f.lower().endswith(".heic")]

def convert(filename):
    src = os.path.join(backup_dir, filename)
    dst = os.path.join(out_dir, filename.rsplit(".", 1)[0] + ".jpg")
    heic_to_jpg(src, dst)

with ThreadPoolExecutor(max_workers=8) as pool:
    pool.map(convert, heic_files)

print(f"Converted {len(heic_files)} files")

The API preserves EXIF metadata including orientation, which means photos render the right way up automatically. No extra rotation logic needed.

Method 2: pillow-heif (recommended local library)

pillow-heif is a Pillow plugin that adds HEIC/HEIF support. It is the cleanest local option because it integrates with Pillow's existing API — you open HEIC the same way you open any other format.

pip install pillow-heif Pillow
# On Linux you may also need: apt-get install libheif1
from PIL import Image
import pillow_heif

pillow_heif.register_heif_opener()  # adds HEIC support to Pillow

def heic_to_jpg(heic_path: str, jpg_path: str, quality: int = 95) -> None:
    img = Image.open(heic_path)
    # Pillow handles EXIF orientation automatically when you save as JPG
    img.convert("RGB").save(jpg_path, "JPEG", quality=quality, exif=img.info.get("exif", b""))

heic_to_jpg("IMG_1234.HEIC", "IMG_1234.jpg")

Three things to watch for:

  • Mode conversion to RGB. HEIC images can be in YCbCr or other color modes. JPG wants RGB. The .convert("RGB") call handles this.
  • EXIF preservation. Pass exif=img.info["exif"] when saving to keep metadata (timestamps, camera make/model, GPS). Without it, all that data is dropped.
  • Apple Live Photos. Some HEIC files are part of a Live Photo pair (image + short video). pillow-heif extracts only the image frame.

Method 3: pyheif (older but works)

pyheif is an older library that wraps libheif more directly. It works but you lose Pillow's familiar API.

pip install pyheif Pillow
apt-get install libheif1 libheif-examples
import pyheif
from PIL import Image

def heic_to_jpg(heic_path: str, jpg_path: str) -> None:
    heif_file = pyheif.read(heic_path)
    img = Image.frombytes(
        heif_file.mode,
        heif_file.size,
        heif_file.data,
        "raw",
        heif_file.mode,
        heif_file.stride,
    )
    # Extract EXIF
    exif_bytes = b""
    for metadata in heif_file.metadata or []:
        if metadata["type"] == "Exif":
            exif_bytes = metadata["data"]
            break
    img.save(jpg_path, "JPEG", quality=95, exif=exif_bytes)

Use pillow-heif instead unless you have a specific reason — it is actively maintained and has a much cleaner API.

Production tips for batch conversion

  • Always preserve EXIF orientation. Without it, portrait-mode iPhone photos display sideways in many viewers. The API does this automatically; for local conversion, use ImageOps.exif_transpose():
from PIL import Image, ImageOps
import pillow_heif

pillow_heif.register_heif_opener()

img = Image.open("photo.HEIC")
img = ImageOps.exif_transpose(img)  # bake rotation into pixels
img.convert("RGB").save("photo.jpg", "JPEG", quality=95)
  • Pick a quality setting deliberately. JPG quality 95 is visually lossless for most content. Quality 85 cuts file size ~40% with imperceptible quality loss for web use. Quality 75 is acceptable for thumbnails.
  • Use Pillow's progressive JPGs for web. save("...", "JPEG", progressive=True) makes the JPG load progressively — better perceived performance on slow connections.
  • Watch memory on big batches. A 12MP HEIC decoded to RGB is ~37MB in memory. Process in chunks if you have thousands of files.

For one-off conversions on your laptop, install pillow-heif and call it a day. For server-side batch jobs at scale, the API removes the libheif system dependency and handles EXIF correctly out of the box. Free tier gives 1,000 conversions/month.