The widget model#

Everything you put on screen is a widget — a button, a label, a frame — and they all follow the same handful of rules: every widget lives in a tree under a parent, is configured through options, carries a set of states, and moves through a lifecycle you manage. Learn these once and every widget behaves predictably.

The widget tree#

A widget’s first argument is its parent (its master). The App root sits at the top; frames and other containers nest under it; leaf widgets like buttons and labels sit inside those. That parent-child chain is the widget tree:

import ttkbootstrap as ttk

app = ttk.App()                       # the root
toolbar = ttk.Frame(app)              # a container under the root
ttk.Button(toolbar, text="Save")      # a button under the toolbar
toolbar.pack()

The tree decides three things: where a widget can be placed (only inside its parent — see Arranging widgets), what it inherits (the theme, and options like a cursor), and its lifetime — destroying a widget destroys everything under it.

Walking the tree#

You never have to keep your own map of what contains what: every widget can tell you. Going up, master is the parent widget:

save.master           # the toolbar Frame -- the widget object itself

Going down, winfo_children() returns a widget’s children as a list, in stacking order:

toolbar.winfo_children()      # [<Button ...!button>, <Button ...!button2>]

for child in toolbar.winfo_children():
    child.state(["disabled"])

There is also a children dict, keyed by each child’s own name — note it’s an attribute, not a method, and the names are Tk’s unless you chose them:

toolbar.children      # {'!button': <Button ...>, '!button2': <Button ...>}

Note

Every widget also has a path name".!frame.!button" — that spells out its position in the tree, and that’s what str(widget) gives you. The path is how Tk — the toolkit tkinter is built on — identifies the widget; What tkinter wraps tells that story. It matters here because winfo_parent() returns the parent’s path, a string, while master returns the widget:

save.master           # <Frame object .!frame>   -- the widget
save.winfo_parent()   # '.!frame'                -- a string

Reach for master when you want the parent. If you only have a path, turn it back into a widget with nametowidget:

save.nametowidget(save.winfo_parent())   # the toolbar Frame

winfo_children() goes one level down only — the root’s children are its frames, not the buttons inside them. To reach the whole tree, recurse:

def walk(widget, depth=0):
    print("   " * depth + widget.winfo_class())
    for child in widget.winfo_children():
        walk(child, depth + 1)

walk(app)
Tk
   TFrame
      TButton
      TButton
   TFrame
      TEntry

winfo_class() reports the widget’s Tk class ("TButton"), which is what that walk prints. Finally, winfo_toplevel() jumps straight to the window a widget lives in, however deep it sits — useful when a callback needs the window but only has the widget. See Widget & screen info for the full introspection set.

Options configure a widget#

Every widget is customized through optionstext, width, image, cursor, and so on. An option is a named setting that lives on the widget; you give it a value when you create the widget, and you can read or change it any time after:

btn = ttk.Button(app, text="Save", width=12)

That is the whole idea. What trips people up is that tkinter gives you two interchangeable spellings for reading and writing options afterward — a method pair, and an index syntax. Both are used freely in real code and throughout these docs, so it is worth learning both now.

Setting an option#

configure takes the option as a keyword argument. The index syntax assigns to the option by name. These two lines do exactly the same thing:

btn.configure(text="Saved")
btn["text"] = "Saved"

Only configure can set several options in one call, which is usually why you would pick it:

btn.configure(text="Saved", width=8, bootstyle="success")

Note

config is an older alias for configure — the same method under a second name, kept for compatibility. You will meet it in older code and in other people’s examples. Prefer configure; these docs use it everywhere.

Reading an option#

cget takes the option name as a string. The index syntax reads the same option. Again, these are the same thing:

btn.cget("text")     # -> "Saved"
btn["text"]          # -> "Saved"

The index syntax is the one that surprises newcomers: a widget is not a dictionary, but it deliberately supports [...] as a shorthand for its options. btn["text"] reads, and btn["text"] = "Saved" writes. Which spelling you use is a matter of taste — pick one and be consistent.

Asking for an option the widget doesn’t have raises TclError, whichever spelling you use:

btn.cget("nope")     # TclError: unknown option "-nope"

The message is Tk’s, in Tk’s own spelling — hence the leading dash on an option you wrote without one. What tkinter wraps explains the seam.

What comes back#

You get back Tk’s value for the option, not necessarily the Python object you passed in. The type follows the option:

btn.cget("text")        # 'Saved'   -- a string
btn.cget("width")       # 8         -- an int, even if you set the string "8"
btn.cget("padding")     # (5,)      -- a tuple

Two options are worth calling out, because the value you get back is a name Tk uses internally rather than the thing you handed over:

btn.cget("command")           # '...<lambda>'  -- not your function
entry.cget("textvariable")    # 'PY_VAR0'      -- not your StringVar

So don’t use cget to get a callback or a variable back out. Keep your own reference to anything you need later — read the value through the variable itself (size.get()), not through the widget. Why Tk keeps a name rather than your object is part of the seam What tkinter wraps describes.

Finding out what a widget accepts#

keys() lists the option names a widget supports — handy at a prompt when you can’t remember the spelling:

btn.keys()          # ['command', 'default', 'takefocus', 'text', ...]

configure() with no arguments returns a dict of every option, mapping the name to its full spec — (name, dbName, dbClass, default, current). Passing a single option name returns just that one spec, which is the way to see an option’s default alongside its current value:

btn.configure("text")     # ('text', 'text', 'Text', '', 'Saved')
#                            name    dbName  dbClass default current

The parts you will actually read are the first and last two — the option’s name, its default, and its current value. dbName and dbClass are the option’s names in Tk’s option database, an older configuration mechanism covered in the Configuration reference.

For the authoritative per-widget option list, each widget’s page in the Widgets catalog and the Widgets reference are the place to look.

Note

bootstyle is just another option. Everything on this page applies — ttk.Button(app, text="Save", bootstyle="success") or btn.configure(bootstyle="success") or btn["bootstyle"] = "success". That’s the whole styling API; see Styling with bootstyle.

States (the ttk way)#

ttk keeps the familiar classic-tkinter state="disabled" option as a shortcut, but underneath, every ttk widget carries a set of state flagsdisabled, active, pressed, focus, selected, readonly, invalid — that you turn on and off. This flag set is the real model: it is what lets a theme restyle a widget per state automatically.

Set a flag with state([...]), clear it by prefixing !, and test one with instate([...]):

btn.state(["disabled"])            # turn on: greys out, stops responding
btn.state(["!disabled"])           # turn off: back to normal

if btn.instate(["disabled"]):      # test a flag
    ...

The same flags drive both behavior and appearance, which is why you never hand-color a disabled or focused widget — the theme already maps every state.

The state="disabled" option is a coarse shortcut over these flags: it understands only normal/disabled (and readonly on entry-style widgets), so state([...]) is the way to reach the rest (active, pressed, invalid, …). One caveat that follows from this: cget("state") reports only what the option was last set to, not the live flags — so after widget.state(["disabled"]) it can still read "normal". To ask whether a flag is set, use instate([...]), never cget("state").

Lifecycle#

A widget passes through three stages: it is created, displayed, and eventually destroyed.

Creating a widget places it in the tree but does not draw it. It exists right away — you can configure and query it — yet it stays invisible until a geometry manager maps it onto the screen:

btn = ttk.Button(app, text="Save")   # exists now, but nothing is drawn
btn.winfo_ismapped()                 # -> False
btn.pack()                           # display it
btn.winfo_ismapped()                 # -> True

That gap is deliberate — you build a whole subtree, then lay it out. It is also why a widget’s master is fixed at creation: you cannot reparent a widget later. To move one, destroy it and rebuild it under the new parent.

Destroying a widget removes it for good with destroy(). The call cascades — destroying a container destroys every widget under it, so tearing down a screen is a single call on its frame. Afterward the widget is gone and its methods raise TclError, so guard with winfo_exists() when a widget might already be destroyed:

panel.destroy()            # removes the panel and all of its children
panel.winfo_exists()       # -> False
# panel.configure(...)     # would raise TclError: invalid command name

Cleaning up. destroy() frees the widget, but not the things you attached around it — a repeating after timer, a variable trace, a binding on another widget. Those keep firing into a widget that no longer exists. Release them as the widget is torn down; the <Destroy> event fires for exactly this:

job = app.after(1000, tick)                       # a repeating timer
widget.bind("<Destroy>", lambda e: app.after_cancel(job))

You rarely write this by hand: ttkbootstrap’s shipped widgets already release their own timers and traces on destroy, and on_close gives the whole app a place to shut down cleanly. Reach for <Destroy> cleanup only for long-lived resources you create yourself. See Events & callbacks for after and traces in depth.

Putting it together#

A small tree, configured, with a state toggled:

import ttkbootstrap as ttk

app = ttk.App(title="Widget model")

panel = ttk.Frame(app, padding=10)
panel.pack()

name = ttk.Entry(panel, width=24)
name.pack(pady=(0, 8))

save = ttk.Button(panel, text="Save", bootstyle="success")
save.pack()

save.state(["disabled"])                 # start disabled
name.bind("<KeyRelease>",
          lambda e: save.state(["!disabled" if name.get() else "disabled"]))

app.mainloop()

See also

Reference#