tkinter: GUI programs in Nil Python
A Nil Python program gets a desktop window with import tkinter, as in
CPython. PXX's tkinter module is lib/pcl/tkinter.pas. It binds the
system's Tcl/Tk 8.6 directly: each widget call becomes one Tcl command, which
is also how CPython's tkinter works underneath. Widgets are objects that take
keyword options, command= and bind take Python functions, and the
widget variables (StringVar, BooleanVar) work as in CPython.
It is a subset of tkinter, not all of it. Everything below was measured on Linux x86-64. A name the module does not have is a compile error that names it, never a call that silently does nothing (see Limits).
For a Pascal program, use the PCL instead.
What you need
- At run time, the Tk 8.6 library:
sudo apt install libtk8.6. No development headers are needed. - To run a GUI program without a desktop (on a server, or in a test), Xvfb:
sudo apt install xvfb, then start the program withxvfb-run -a.
An example
A window with an entry, a check box, a button and a label. The button's
command is a Python function. Instead of waiting for a person, after()
types a name, presses the button, ticks the box, prints what the widgets now
hold, and closes the window, so the program also runs headless.
import tkinter as tk
root = tk.Tk()
root.title("tkinter hello")
name = tk.StringVar(value="")
remember = tk.BooleanVar(value=False)
entry = tk.Entry(root, textvariable=name, width=20)
entry.pack(padx=8, pady=4)
check = tk.Checkbutton(root, text="Remember me", variable=remember)
check.pack()
greeting = tk.Label(root, text="(nobody yet)")
greeting.pack(pady=4)
def greet():
greeting.configure(text="Hello, " + name.get())
button = tk.Button(root, text="Greet", command=greet)
button.pack(pady=4)
def play():
name.set("PXX")
button.invoke()
remember.set(True)
print("entry:", entry.get())
print("label:", greeting.cget("text"))
print("checked:", remember.get())
print("children:", len(root.winfo_children()))
root.destroy()
root.after(300, play)
root.mainloop()
print("closed")
./pxx tk_hello.npy tk_hello
xvfb-run -a ./tk_hello
entry: PXX
label: Hello, PXX
checked: True
children: 4
closed
Built with pin v451 (compiler sha256 d9b7226769cc) through ./pxx and run
under xvfb-run on 2026-09-29. CPython 3.14.4 prints the same five lines for
the same file. Drop the after() line to keep the window open.
More programs are in examples/tk/, listed on the
Examples page.
What works
The module has these names. Each widget takes the options listed in
lib/pcl/tkinter.pas as keyword arguments, in any subset.
| Area | Names |
|---|---|
| Windows | Tk (with className=, the window class), Toplevel (transient, grab_set, grab_release); on both, geometry, minsize, maxsize, resizable, iconphoto and wait_window (a modal dialog) |
| Widgets | Frame, Label, Entry, Button, Checkbutton, Text, Canvas, Scrollbar, Menu, Separator, PanedWindow, Notebook, PhotoImage |
| Variables | StringVar, BooleanVar (get, set, trace_add) |
| Layout | pack (with anchor=), grid, grid_columnconfigure/columnconfigure, grid_rowconfigure/rowconfigure (with uniform=), pack_forget, grid_forget, pack_propagate, grid_propagate |
| Every widget | configure/config (with cursor=), cget, bind, bind_all, unbind_all, event_generate (with x=, y=), after, after_idle, after_cancel, winfo_children, winfo_width, winfo_height, winfo_class, winfo_ismapped, focus_set, destroy, update, mainloop, quit, the clipboard calls |
Text |
insert, delete, get, index, see, mark_set, tag_add, tag_configure, scrolling |
Canvas |
create_text, create_line, create_polygon, create_rectangle, create_oval, create_image, create_window, each with tags=; coordinates as four numbers, a flat list or a list of (x, y) pairs; coords, itemconfigure/itemconfig, itemcget, find_all, find_withtag, tag_raise, tag_lower, bbox, delete (by id or tag), scrolling with yview_moveto |
| Dialogs | from tkinter import messagebox: showinfo, showwarning, showerror, askyesno, askokcancel, askyesnocancel. from tkinter import filedialog: askopenfilename, asksaveasfilename, askdirectory |
| ttk | from tkinter import ttk: ttk.Frame (with padding=), ttk.Label, ttk.Entry, ttk.Button, ttk.Checkbutton, ttk.Notebook, ttk.PanedWindow, ttk.Separator, ttk.Scrollbar. ttk.Style() with theme_use, theme_names, configure(name, background=, foreground=, font=, relief=, padding=, ...) and map(name, background=[(state, value), ...], ...), and style= on ttk.Frame and ttk.Label |
| Option names | the short bg= and fg= wherever background= and foreground= are taken (Frame, Label, Canvas, Toplevel, configure/config) |
| Constants | tk.END, tk.BOTH, tk.LEFT, tk.RIGHT, tk.TOP, tk.BOTTOM, tk.HORIZONTAL, tk.VERTICAL, tk.WORD, tk.CENTER, tk.NW, tk.NE, tk.SW, tk.SE, tk.DISABLED, tk.NORMAL (the same strings as CPython's); the error class tk.TclError |
tkinter.font.Font(family=, size=, weight=, slant=) or Font(font=spec) has
measure, metrics and actual, and a Font can be passed as any widget's
font=. It is a shim, and
from tkhtmlview import HTMLLabel, HTMLScrolledText renders HTML into a Tk
text widget (see Nil Python: Shims).
A command or binding can be a plain function, a bound method or a lambda. A
bound function receives an event object with tkinter's field names, such as
event.width for <Configure> and event.widget, the widget the binding was
made on (None for bind_all), and a yscrollcommand function receives
Tk's two fractions as strings, as in CPython. examples/tk/callbacks.npy
shows each of these, and it printed its .expected output with v451.
A class can extend a widget and pass options on with
super().__init__(parent, height=h, **kwargs). When Tk cannot start, for
example with no display, tk.Tk() raises tk.TclError with Tk's reason, so
the usual except tk.TclError: fallback to a text interface works.
examples/tk/style_and_short_options.npy and
examples/tk/no_display_raises_tclerror.npy match CPython's output; both were
checked on 2026-10-20 with the compiler built from master (not yet in a pin).
So was examples/tk/canvas_items_dialog_and_events.npy: a live graph moved with
coords(), a scrolling side bar of clickable tiles, Style.map, font
measuring and a modal dialog closed through wait_window. And so was
examples/tk/sidebar_cards_and_hover_rows.npy: card frames with a highlight
border, wrapped labels, uniform grid columns, rows taken out with
pack_forget, a window icon, and a group of widgets bound with lambdas in a
loop to act as one hover button.
coords(item, x0, y0, ...) takes at most four numbers. For a longer polyline
pass the list, coords(item, pts), which CPython takes too.
Limits
Measured with pin v451 on 2026-09-29, each with a one-line program:
- Not in the module:
Listbox,Radiobutton,Scale,Spinbox,IntVarandDoubleVar, among others.tk.Listbox(root)stops the build with "no member Listbox came of the qualifier tk". from tkinter import *does not compile ("expected expression"). This is true of every module in Nil Python, not only tkinter. Import the names you use, or useimport tkinter as tk.tk.Xandtk.Yare not there ("no member X came of the qualifier tk"). Write the option as a string:pack(fill="x").- Write
from tkinter import messagebox, notimport tkinter.messagebox. The dotted form is refused ("no unit named tkinter_messagebox"). - Only Linux x86-64 has been run. Tk is a system library, so a program
needs
libtk8.6wherever it runs. The binary nameslibtcl8.6andlibtk8.6as required libraries, so without them it does not start at all: anexcept ImportErroraroundimport tkintercannot catch their absence. aarch64 cross-compiles the same way but has not been run. - Memory: with v451, every event delivered to a bound function leaked its
event object, and each
winfo_children()call leaked each child it returned. A program that handled 300<Configure>events (the census build ofexamples/tk/event_and_children_are_released.npy) ended with 2,431 allocations still live on v451, against 25 after the fix. Fixed after v451 (6658668a8b, in no pin yet).