A bakery owner wanted a register-side window that cycles through menu photos. That’s a Tkinter image job: load the file, wrap it in a PhotoImage, show it on a Label.
This is the finished viewer, running for real:

In this tutorial, I’ll build that Tkinter image viewer step by step on Python 3.12.5 with Tk 8.6 and Pillow 12.3.0, then fix the three bugs I hit.
Before You Start: install Pillow for Tkinter images
Tkinter ships with Python, but you’ll need Pillow for JPG files and resizing:
pip install pillow
You can use any images you have. I generated these with Pillow and kept them next to the script:
| File | Size (px) | Menu item |
|---|---|---|
| logo.png | 120 x 120 | Shop logo |
| muffin.jpg | 1600 x 1067 | Blueberry Muffin, $3.75 |
| croissant.jpg | 1600 x 1067 | Butter Croissant, $4.25 |
| pie.jpg | 1600 x 1067 | Apple Pie Slice, $5.50 |
Step 1: Show a PNG image with tk.PhotoImage
Tkinter’s own PhotoImage reads PNG and GIF, so the logo needs no extra package:
import tkinter as tk
root = tk.Tk()
root.title("Maple Street Bakery")
root.geometry("560x230")
logo = tk.PhotoImage(file="logo.png") # PNG/GIF, no Pillow needed
logo_label = tk.Label(root, image=logo)
logo_label.pack(pady=(24, 8))
tk.Label(root, text="Maple Street Bakery",
font=("Segoe UI", 16, "bold")).pack()
root.mainloop()

To put text beside the image in one widget, use compound from my Tkinter Label tutorial.
Checkpoint: logo.width() returns 120.
Step 2: Load and resize a JPG image with Pillow
Pillow opens and shrinks the 1600 px JPG, and ImageTk.PhotoImage converts it for Tkinter:
import tkinter as tk
from PIL import Image, ImageTk
root = tk.Tk()
root.title("Blueberry Muffin")
root.geometry("600x400")
img = Image.open("muffin.jpg")
print("Original size:", img.size)
img.thumbnail((540, 340)) # shrink, keep the aspect ratio
print("Shown at:", img.size)
photo = ImageTk.PhotoImage(img) # keep this reference
photo_label = tk.Label(root, image=photo)
photo_label.pack(pady=20)
root.mainloop()

thumbnail() keeps the aspect ratio and never enlarges; resize((w, h)) forces an exact size. To save converted files, see the JPG to PNG converter in Python.
Checkpoint: the console prints Shown at: (510, 340).
Step 3: Add Previous and Next buttons to swap the image
Swap the single photo for a menu list, show() and two buttons (same imports and window):
MENU = [("muffin.jpg", "Blueberry Muffin", 3.75),
("croissant.jpg", "Butter Croissant", 4.25),
("pie.jpg", "Apple Pie Slice", 5.50)]
photo_label = tk.Label(root)
photo_label.pack(pady=(16, 8))
caption = tk.Label(root, font=("Segoe UI", 13, "bold"))
caption.pack()
index = 0
photo = None # keeps the current image alive
def show(i):
global index, photo
index = i % len(MENU) # wrap around at both ends
file, name, price = MENU[index]
img = Image.open(file)
img.thumbnail((540, 340))
photo = ImageTk.PhotoImage(img)
photo_label.config(image=photo)
caption.config(text=f"{name} - ${price:.2f} "
f"({index + 1} of {len(MENU)})")
buttons = tk.Frame(root)
buttons.pack(pady=12)
prev_btn = tk.Button(buttons, text="< Previous", width=12,
command=lambda: show(index - 1))
prev_btn.pack(side="left", padx=6)
next_btn = tk.Button(buttons, text="Next >", width=12,
command=lambda: show(index + 1))
next_btn.pack(side="left", padx=6)

config(image=photo) swaps the picture in place, and global photo keeps it alive after show() returns. % wraps the index. More in the Python Tkinter Button guide.
Test the Tkinter image viewer
Run it and press Previous on the first photo. That’s the edge case most likely to break:

Checkpoint: the caption reads Apple Pie Slice - $5.50 (3 of 3).
The window stays 600 x 480 for every photo; to change that, see how to set the Tkinter window size.
Why does a Tkinter image show blank or raise an error?
The image is blank. A PhotoImage held only by a local variable is deleted when the function returns:
root = tk.Tk()
root.title("Butter Croissant")
root.geometry("600x400")
photo_label = tk.Label(root, bg="#e2e8f0")
photo_label.pack(pady=20)

photo_label.image = photo keeps it.JPG fails with TclError. Plain tk.PhotoImage on Tk 8.6 can’t read JPG. The real run in VS Code:
import tkinter as tk
root = tk.Tk()
try:
photo = tk.PhotoImage(file="muffin.jpg")
except tk.TclError as err:
print("TclError:", err)
Output:
TclError: couldn't recognize data in image file "muffin.jpg"

ANTIALIAS is gone. Pillow 10 removed Image.ANTIALIAS:
from PIL import Image
img = Image.open("muffin.jpg")
small = img.resize((300, 200), Image.ANTIALIAS)
Output:
AttributeError: module 'PIL.Image' has no attribute 'ANTIALIAS'
Note: use Image.LANCZOS, the same filter under its current name.
Where to take it next: Tkinter image file dialogs and backgrounds
Add a file dialog, as in uploading a file in Python Tkinter, or put a photo behind a form by setting a background image in Tkinter.
More Tkinter reading from the site:
- Python GUI programming with Tkinter
- Tkinter Checkbutton in Python
- Tkinter pack vs grid
- Tkinter Frame with grid
Frequently asked questions
How do I display an image in Tkinter?
Pass a tk.PhotoImage to a Label’s image= option and keep a reference.
Why can’t Tkinter open my JPG file?
Tk 8.6 reads PNG, GIF and PPM only. Use Pillow’s ImageTk.PhotoImage.
How do I resize an image in Tkinter?
Resize with Pillow first: thumbnail() or resize().
Why does my Tkinter image not show up?
It was garbage-collected. Keep it, for example label.image = photo.
How do I change the image in a Label?
Call label.config(image=new_photo) and keep the new reference.
Where is PhotoImage documented?
See tkinter images in the Python docs.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn