PhotoImage Class Tutorial: Build a Simple Tkinter Image Viewer

10 min read

The PhotoImage class is Tkinter’s built-in way to load an image and show it in a Python desktop app. To build a viewer with it, you load a file with tk.PhotoImage(file=path), attach the result to a Label, and keep a reference to that object so Python doesn’t delete it. Add a folder picker and two buttons, and you have a working photo browser.

That’s all this project takes: about 80 lines of Python and no extra packages. It’s also a good way to learn the PhotoImage class, because a viewer forces you to deal with the problems real apps run into, like unsupported files and oversized photos. By the end, you’ll have an app that opens a folder, steps through its images with buttons or arrow keys, and shrinks large pictures to fit the window.

What the Finished Viewer Does

Here’s the feature list, so you know what you’re building:

  • Opens a folder through your system’s normal folder picker
  • Lists the PNG, GIF, PPM, and PGM files inside it
  • Moves through them with Previous and Next buttons or the arrow keys
  • Loops back to the first image after the last one
  • Shrinks images that are too large for the window
  • Shows the file name and position, like “beach.png (3 of 12)”
  • Names a broken file in the status bar and keeps running

What You Need Before You Start

You need Python 3 with Tkinter installed. The standard Windows and macOS installers from python.org usually include it, but many Linux distributions package it separately. On Ubuntu or Debian, run sudo apt install python3-tk.

Then check your Tk version from a terminal:

bash

python3 -c "import tkinter; print(tkinter.TkVersion)"

If you see 8.6 or higher, you’re set. The official Python Tkinter documentation says PNG support starts with Tk 8.6, so an older build would be limited to GIF, PGM, and PPM. On Windows, use python instead of python3 if the command isn’t found.

File Formats the PhotoImage Class Can Open

This trips up a lot of beginners, because the most common photo format isn’t on the list.

FormatStandard TkinterWith Pillow
PNGYes (Tk 8.6+)Yes
GIFYesYes
PPM / PGMYesYes
JPEGNoYes
BMP, TIFF, WebPNoYes

If you try to open a JPEG with the PhotoImage class alone, you’ll get an error like couldn't recognize data in image file. That message makes the file sound damaged, but the format just isn’t supported. Pillow fixes this, and there’s a short upgrade for it near the end.

The Complete Image Viewer Code

Save this as viewer.py. Read through it once, then we’ll go over the parts that matter.

python

import math
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox

SUPPORTED = {".png", ".gif", ".ppm", ".pgm"}
MAX_WIDTH = 800
MAX_HEIGHT = 500


class ImageViewer:
    def __init__(self, root):
        self.root = root
        self.root.title("Simple Image Viewer")
        self.files = []
        self.index = 0
        self.photo = None  # keeps the current image alive

        self.image_label = tk.Label(root, text="Open a folder to begin", bg="#222", fg="white")
        self.image_label.pack(fill="both", expand=True)

        controls = tk.Frame(root)
        controls.pack(fill="x", pady=6)
        tk.Button(controls, text="Open Folder", command=self.open_folder).pack(side="left", padx=6)
        tk.Button(controls, text="Previous", command=self.previous).pack(side="left")
        tk.Button(controls, text="Next", command=self.next).pack(side="left", padx=6)
        self.status = tk.Label(controls, text="No folder loaded")
        self.status.pack(side="right", padx=6)

        root.bind("<Left>", lambda event: self.previous())
        root.bind("<Right>", lambda event: self.next())

    def open_folder(self):
        folder = filedialog.askdirectory(title="Choose a folder of images")
        if folder:
            self.load_folder(folder)

    def load_folder(self, folder):
        self.files = sorted(
            p for p in Path(folder).iterdir()
            if p.suffix.lower() in SUPPORTED
        )
        self.index = 0
        if self.files:
            self.show_image()
        else:
            messagebox.showinfo("No images", "No PNG, GIF, PPM or PGM files found.")

    def show_image(self):
        path = self.files[self.index]
        try:
            image = tk.PhotoImage(file=str(path))
        except tk.TclError as error:
            self.status.config(text=f"Can't open {path.name}")
            print(error)
            return

        scale = max(
            math.ceil(image.width() / MAX_WIDTH),
            math.ceil(image.height() / MAX_HEIGHT),
            1,
        )
        if scale > 1:
            image = image.subsample(scale)

        self.photo = image
        self.image_label.config(image=self.photo, text="")
        self.status.config(text=f"{path.name} ({self.index + 1} of {len(self.files)})")

    def next(self):
        if self.files:
            self.index = (self.index + 1) % len(self.files)
            self.show_image()

    def previous(self):
        if self.files:
            self.index = (self.index - 1) % len(self.files)
            self.show_image()


if __name__ == "__main__":
    root = tk.Tk()
    root.geometry("840x580")
    ImageViewer(root)
    root.mainloop()

Run it with python3 viewer.py, click Open Folder, and pick a folder with a few PNG files in it.

How the Code Works

Building the Window

The window has two areas. The Label at the top holds the picture, and fill="both", expand=True lets it take any extra space when the window grows. The Frame below it groups the three buttons and the status text along one row.

Using a Label for the picture is the simplest choice for a viewer. A Canvas is the better pick if you later want zooming, dragging, or drawing on top of an image, but it adds code you don’t need here.

Finding the Image Files

askdirectory() returns the chosen folder as a string, or an empty string if the user cancels. That’s why open_folder checks if folder: before doing anything else. The Python docs cover this and the other Tkinter file dialogs if you’d rather let people pick a single file.

load_folder then keeps only the files with a supported extension. Calling .lower() matters more than it looks: without it, HOLIDAY.PNG gets skipped, and you’d spend ten minutes wondering where it went.

One small quirk is that sorted() compares names as text, so img10.png lands before img2.png. Zero-padded names like img02.png avoid the problem.

Loading the Picture With the PhotoImage Class

All the image work happens in show_image, and this line is where the PhotoImage class comes in:

python

image = tk.PhotoImage(file=str(path))

It reads the file into memory and creates an image object that Tk can draw. The try block catches tk.TclError, which is what you get for a corrupt file or one with a misleading extension. Without it, a bad file would leave the screen unchanged, with only a traceback in the terminal to explain why.

When a file fails, the last good image stays on screen and the status bar names the problem. Press Next and you move on normally.

Why self.photo Matters

This one line prevents the most common PhotoImage class bug:

python

self.photo = image

According to the Python documentation, Tk doesn’t keep its own reference to an image. Once the last Python reference is deleted, the image data goes with it, and the widget shows an empty box.

Here’s the classic broken version:

python

def show_broken(root):
    image = tk.PhotoImage(file="photo.png")
    tk.Label(root, image=image).pack()

No error appears. The Label is created, then image is deleted when the function ends, and the picture never shows up. Storing it on self keeps it alive as long as the viewer needs it. Each new image then replaces the old one, so a long browsing session doesn’t pile up every photo in memory.

Shrinking Large Images to Fit

The PhotoImage class has no resize method that accepts a target size. You get subsample(), which shrinks an image by keeping every Nth pixel, and zoom(), which enlarges it by repeating pixels. Both need whole numbers. Passing 2.5 raises TclError: expected integer but got "2.5".

So the code works out how many times too large the image is in each direction, rounds up, and uses the bigger result. Here’s what that does with an 800 × 500 limit:

Original sizeWidth ratioHeight ratioScale usedDisplayed size
1600 × 10002.02.02800 × 500
2000 × 6002.51.23667 × 200
640 × 4800.80.961640 × 480

Look at the second row. A true fit would be 800 × 240, but since 2.5 isn’t allowed, the image drops to 667 × 200. It’s a real limit of this approach: pictures can come out noticeably smaller than the space available, and skipped pixels can leave fine lines looking jagged.

Moving Between Images

The % operator handles the looping in both next and previous. If you’re on the last of 12 images (index 11), (11 + 1) % 12 gives 0, which takes you back to the start. Going backward from index 0, (0 - 1) % 12 gives 11, since Python’s modulo result takes the sign of the divisor.

The if self.files: check stops an arrow key press from raising an error before a folder is open.

Add JPEG Support and Smoother Resizing With Pillow

Most phone and camera photos are JPEGs, so this is the first upgrade worth making. Pillow’s ImageTk module provides its own PhotoImage, which works anywhere Tkinter expects an image. Install it with pip install pillow, then make three changes.

Add the import at the top and widen the file list:

python

from PIL import Image, ImageTk

SUPPORTED = {".png", ".gif", ".ppm", ".pgm", ".jpg", ".jpeg", ".bmp", ".webp"}

Then replace the whole loading and scaling section of show_image, from try down to the end of the if scale > 1: block, with this:

python

try:
    with Image.open(path) as picture:
        picture.thumbnail((MAX_WIDTH, MAX_HEIGHT))
        image = ImageTk.PhotoImage(picture)
except OSError as error:
    self.status.config(text=f"Can't open {path.name}")
    print(error)
    return

thumbnail() keeps the proportions and fits the image to the limit at any size, so that 2000 × 600 picture now fills the full 800 pixels of width. You can delete import math too, since nothing uses it anymore. The self.photo = image rule still applies, because Pillow’s object can be garbage collected in exactly the same way.

The rest of the code stays as it is, though you may want to reword the “No images” message to match the longer format list. This Pillow version is also the better base for a real photo app, because the PhotoImage class on its own will always be limited to whole-number scaling.

PhotoImage Class Methods Worth Knowing

The viewer only uses a few methods, but the PhotoImage class has more that are handy once the basics work. These are the ones you’re most likely to reach for next:

MethodWhat it doesWhere you might use it
width(), height()Return the size in pixelsWorking out a scale, as the viewer does
subsample(x)Returns a smaller copyFitting large images into a window
zoom(x)Returns a larger copyEnlarging pixel art or tiny icons
copy()Returns a duplicateKeeping an original while you change a version
get(x, y), put(data, to)Read or set pixelsSimple filters and generated images
write(filename)Saves the image to a fileExporting an edited result

Note that subsample() and zoom() return new objects and leave the original alone. That’s why the viewer writes image = image.subsample(scale) instead of calling the method on its own.

Common Problems and Quick Fixes

What you seeLikely causeFix
Blank area, no errorImage reference was deletedStore the PhotoImage class object on self
couldn't recognize data in image fileJPEG, unsupported format, or corrupt fileUse Pillow, or check the file itself
couldn't open "...": no such file or directoryWrong path or wrong working directoryUse a full path or build it with pathlib
expected integer but got "2.5"Decimal passed to subsample() or zoom()Round to a whole number, or use Pillow
No module named 'tkinter'Tkinter isn’t installedInstall python3-tk on Linux

Where to Take It Next

Your viewer now covers the essentials of the PhotoImage class: loading, displaying, scaling, and keeping images alive. That’s most of what everyday Tkinter image work comes down to. Good next steps are a delete or rename button, a slideshow built with root.after(), or rescaling when the window is resized. Pick one, since each of them will teach you something different about how Tkinter handles events.

If you plan to go further with the PhotoImage class, the community-maintained TkDocs PhotoImage reference lists every available method, including transparency_get() and copy_replace().

Frequently Asked Questions

Can the Tkinter PhotoImage class open JPG files?

Not on its own. The PhotoImage class opens PNG, GIF, PGM, and PPM files. For JPEG, install Pillow and use ImageTk.PhotoImage instead, which works with the same widgets.

Why does my Tkinter image not show up?

The usual cause is a lost reference. If the image is stored in a local variable, Python deletes it when the function ends, leaving a blank widget. Save it to an instance attribute such as self.photo.

What’s the difference between tk.PhotoImage and ImageTk.PhotoImage?

tk.PhotoImage is the PhotoImage class built into Tkinter, and it needs no installation. ImageTk.PhotoImage comes from Pillow, which adds formats like JPEG and WebP and lets you resize images with Pillow’s tools before displaying them.

How do I resize an image in Tkinter?

The PhotoImage class offers subsample() to shrink and zoom() to enlarge, both by whole-number factors. For an exact size, resize the image in Pillow first, then pass it to ImageTk.PhotoImage.

Leave a comment

Your email address will not be published. Required fields are marked *