Short answer

width, height = Image.open("photo.jpg").size. Also available without decoding pixels: img.format (JPEG, PNG...), img.mode (RGB, RGBA...) and img.info (DPI, ICC profile and other metadata). For the file size on disk, use os.path.getsize().

Size, format and mode

image_info.py
from PIL import Image

with Image.open("photo.jpg") as img:
    width, height = img.size
    print("size:  ", width, "x", height)
    print("format:", img.format)            # file format: JPEG, PNG, WEBP...
    print("mode:  ", img.mode)              # pixel layout: RGB, RGBA, L...
    print("bands: ", img.getbands())        # channel names
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
size:   1600 x 1067
format: JPEG
mode:   RGB
bands:  ('R', 'G', 'B')

img.width and img.height are shortcuts for the two parts of img.size. The with block closes the file when you are done, which matters when you process many files.

Everything else you can read without loading the pixels

more_info.py
import os
from PIL import Image

path = "photo.jpg"
with Image.open(path) as img:
    print("file size:     ", os.path.getsize(path), "bytes")
    print("in memory:     ", img.width * img.height * len(img.getbands()), "bytes when decoded")
    print("dpi:           ", img.info.get("dpi"))
    print("ICC profile:   ", "yes" if img.info.get("icc_profile") else "no")
    print("animated:      ", getattr(img, "is_animated", False), getattr(img, "n_frames", 1))
    print("MIME type:     ", Image.MIME.get(img.format))
    print("info keys:     ", sorted(img.info))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
file size:      239040 bytes
in memory:      5121600 bytes when decoded
dpi:            None
ICC profile:    yes
animated:       False 1
MIME type:      image/jpeg
info keys:      ['icc_profile', 'jfif', 'jfif_density', 'jfif_unit', 'jfif_version']

The decoded size matters for memory: this 233 KB JPEG needs 4.9 MB once decoded, about 21× its size on disk. A 6000×4000 photo takes about 72 MB once decoded, so opening many large images at once adds up quickly.

Phone photos: width and height can be swapped

img.size reports the pixels as stored. Phones often store portrait photos in landscape and add an EXIF tag saying "rotate when displaying", so the width and height you get are swapped compared with what you see:

phone_photo.py
from PIL import Image, ImageOps

with Image.open("phone.jpg") as img:
    print("stored size:   ", img.size)
    print("orientation:   ", img.getexif().get(0x0112))
    print("displayed size:", ImageOps.exif_transpose(img).size)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
stored size:    (1200, 800)
orientation:    6
displayed size: (800, 1200)

To get the displayed size without decoding the image, check the orientation tag yourself:

displayed_size.py
from PIL import Image


def displayed_size(path):
    """Width and height as viewers show the image (EXIF rotation applied), without decoding pixels."""
    with Image.open(path) as img:
        w, h = img.size
        if img.getexif().get(0x0112) in (5, 6, 7, 8):      # these orientations rotate by 90 degrees
            w, h = h, w
        return w, h


print(displayed_size("phone.jpg"), displayed_size("photo.jpg"))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(800, 1200) (1600, 1067)

More on orientation in the resizing guide and the EXIF guide.

Scan a whole folder quickly

Because only headers are read, scanning thousands of files takes seconds:

scan_folder.py
import time
from pathlib import Path
from PIL import Image

start = time.perf_counter()
rows = []
for path in sorted(Path("photos").iterdir()):
    with Image.open(path) as img:            # header only: no pixels are decoded
        rows.append((path.name, img.format, img.mode, img.size, path.stat().st_size // 1024))
elapsed = (time.perf_counter() - start) * 1000

for name, fmt, mode, size, kb in rows:
    print(f"{name:<12} {fmt:<5} {mode:<4} {str(size):<13} {kb:>5} KB")
print(f"scanned {len(rows)} files in {elapsed:.1f} ms")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
banner.png   PNG   RGB  (900, 600)      539 KB
img_1.jpg    JPEG  RGB  (1600, 1067)    185 KB
img_2.jpg    JPEG  RGB  (1067, 1600)    202 KB
img_3.jpg    JPEG  RGB  (2400, 1350)    441 KB
scanned 4 files in 11.8 ms

How big would it be as JPEG, PNG or WebP?

To know the encoded size before writing anything to disk, save into memory:

encoded_size.py
import io
from PIL import Image

img = Image.open("photo.jpg")
for fmt, opts in [("JPEG", {"quality": 85}), ("PNG", {}), ("WEBP", {"quality": 80})]:
    buf = io.BytesIO()
    img.save(buf, fmt, **opts)
    print(f"{fmt:<5} would be {buf.tell() / 1024:7.1f} KB")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
JPEG  would be   188.9 KB
PNG   would be  1295.1 KB
WEBP  would be    78.3 KB

Why img.format is sometimes None

format describes the file an image was read from. Any image you create or derive (by resizing, converting, cropping) has no file behind it, so its format is None:

format_none.py
from PIL import Image

img = Image.open("photo.jpg")
print("opened:  ", img.format)
small = img.resize((100, 67))
print("resized: ", small.format)            # derived images have no file format
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
opened:   JPEG
resized:  None

Keep the original's format in a variable if you need to save the result in the same format.

The outputs and images on this page are the real results of running every example with Pillow 12.3.0 on Python 3.13 (Linux) on 2026-10-15. Every example is also re-run automatically on macOS (Darwin 25.6.0, Pillow 12.3.0), Linux (Linux 6.17.0-1022-azure, Pillow 12.3.0), Windows (Windows 2025Server, Pillow 12.3.0); the line under each example shows the result per operating system. The DejaVu Sans font files sit next to each script so font examples behave the same everywhere. Sample photos: NASA/Sam Lott, public domain. Spotted something wrong? Tell us.