2025, Nov 18 15:00
Display a 16x16 PNG icon in an X11 window with python-xlib and Pillow: avoid X.Image, use put_pil_image
Learn how to render a 16x16 PNG in an X11 window using python-xlib and Pillow. Fix the X.Image AttributeError, use screen.root_visual, and draw with put_pil_image in a concise working example
Rendering tiny window icons in a Python-based X11 prototype sounds easy until you meet the moving target of what a given binding actually exposes. If you tried to construct an XImage directly and push raw bytes through python-xlib, you probably ran into an exception like “module 'Xlib.X' has no attribute 'Image'”. Below is a compact walkthrough of the underlying cause and a practical path that works with Pillow and python-xlib.
Problem setup
The goal is to show a 16×16 PNG inside an X11 window created from Python 3 using Pillow and python-xlib. A straightforward approach is to open the PNG with PIL, convert it to RGB bytes, build an XImage, and send it to the window with put_image. That’s where things fall apart.
Failing example
The following minimal program initializes the display, creates a window, converts a PNG into raw bytes, and then attempts to create an X.Image to blit it. It also auto-generates a red 16×16 test image on first run.
from Xlib import display, X # Xutil is not required here
from PIL import Image, ImageDraw
import os
def draw_icon(img_path, win_w, win_h, pos_x=50, pos_y=50):
pil_img = Image.open(img_path)
if pil_img.mode != 'RGB':
pil_img = pil_img.convert('RGB')
rgb_buf = pil_img.tobytes("raw", "RGB")
dpy = display.Display()
scr = dpy.screen()
root_win = scr.root
vis = scr.root_visual # previously tried "default_visual"
depth_bits = scr.root_depth
wnd = root_win.create_window(
pos_x, pos_y,
win_w, win_h,
1,
depth_bits,
X.InputOutput,
vis,
background_pixel=scr.white_pixel,
event_mask=X.ExposureMask | X.KeyPressMask | X.StructureNotifyMask
)
wnd.set_wm_name("PNG image")
wnd.map()
gctx = wnd.create_gc(
foreground=scr.black_pixel,
background=scr.white_pixel
)
# This is where it blows up: X.Image doesn't exist in python-xlib
ximg = X.Image(
data=rgb_buf,
width=16,
height=16,
depth=depth_bits,
bitmap_unit=8,
bitmap_pad=32,
byte_order=X.LSBFirst,
bitmap_bit_order=X.LSBFirst,
format=X.ZPixmap,
bytes_per_line=16 * 3
)
while True:
evt = dpy.next_event()
if evt.type == X.Expose:
if evt.count == 0:
wnd.put_image(gctx, ximg, 0, 0, 0, 0, 16, 16)
dpy.flush()
elif evt.type == X.KeyPress:
break
elif evt.type == X.ConfigureNotify:
pass
wnd.destroy()
dpy.close()
if __name__ == '__main__':
sample = "dummy.png"
if not os.path.exists(sample):
tmp = Image.new('RGB', (16, 16), color='red')
painter = ImageDraw.Draw(tmp)
painter.text((1, 1), "abc", fill=(255, 255, 255))
tmp.save(sample)
draw_icon(sample, 200, 200, 100, 100)
Running this yields:
AttributeError: module 'Xlib.X' has no attribute 'Image'
What’s actually wrong
There are two separate issues. First, one might search for default_visual on the screen object; it’s not there, while root_visual is. Second, X.Image is not provided by python-xlib. Instead of constructing an XImage yourself, there is a built-in helper that takes a PIL.Image directly and handles the transfer for you.
The fix
Use screen.root_visual instead of a missing default_visual, and replace the manual XImage path with the window’s put_pil_image method. This removes the need to convert to raw bytes and avoids the absent X.Image entirely.
from Xlib import display, X
from PIL import Image, ImageDraw
import os
def blit_png_to_window(img_path, win_w, win_h, pos_x=50, pos_y=50):
pil_img = Image.open(img_path)
if pil_img.mode != 'RGB':
pil_img = pil_img.convert('RGB')
dpy = display.Display()
scr = dpy.screen()
root_win = scr.root
vis = scr.root_visual
depth_bits = scr.root_depth
wnd = root_win.create_window(
pos_x, pos_y,
win_w, win_h,
1,
depth_bits,
X.InputOutput,
vis,
background_pixel=scr.white_pixel,
event_mask=X.ExposureMask | X.KeyPressMask | X.StructureNotifyMask
)
wnd.set_wm_name("PNG preview")
wnd.map()
gctx = wnd.create_gc(
foreground=scr.black_pixel,
background=scr.white_pixel
)
while True:
evt = dpy.next_event()
if evt.type == X.Expose:
if evt.count == 0:
wnd.put_pil_image(gctx, 0, 0, pil_img)
dpy.flush()
elif evt.type == X.KeyPress:
break
elif evt.type == X.ConfigureNotify:
pass
wnd.destroy()
dpy.close()
if __name__ == '__main__':
sample = "dummy.png"
if not os.path.exists(sample):
tmp = Image.new('RGB', (16, 16), color='red')
painter = ImageDraw.Draw(tmp)
painter.text((1, 1), "abc", fill=(255, 255, 255))
tmp.save(sample)
blit_png_to_window(sample, 200, 200, 100, 100)
Why this matters
When you explore a low-level API via a Python binding, not every native concept is surfaced one-to-one. Trying to force an unavailable X.Image path results in dead ends, while leaning on an existing helper like put_pil_image yields a working pipeline with less code and fewer footguns. If you’re experimenting with a Python window manager, knowing which primitives the binding exposes will save hours of trial and error. There are also projects in the Python ecosystem that take different routes; for example, one window manager uses xcffib on top of XCB.
Practical advice
If you’re unsure whether a field exists on python-xlib objects, introspection helps. Checking attributes with dir(...) or inspecting available keys in an object’s internal data can point you to what's actually present, such as finding root_visual instead of expecting default_visual. For pushing pixel data, prefer the binding’s direct integration with Pillow rather than hand-rolling XImage structures that aren’t available.
Wrap-up
To render 16×16 PNG icons in an X11 window via python-xlib, avoid non-existent constructs like X.Image and rely on window.put_pil_image with a PIL.Image. Use screen.root_visual where applicable, and keep the event loop minimal, updating on Expose and exiting on KeyPress. This keeps the code concise, robust, and aligned with what the library offers today.