Chapter 2 · GTK 4 with PyGObject
GObject: Properties, Signals and Bindings
Every listing in this chapter is a file under
examples/gtk4/gobject/. They are run on each build, so if one of them stops working the build says so.
Introduction
GTK is written in C, and C has no objects. GObject is the object system that was built to give it some — classes, inheritance, virtual methods, properties, signals and runtime type information, in plain C.
You could reasonably ask why a Python programmer should care. Python already has all of that.
The answer is that GObject’s versions of those things are the ones GTK can see.
A Python attribute is invisible to GTK. A GObject property can be watched for
changes, bound to another object’s property, read by a Gtk.Expression, sorted
and filtered on, set from a .ui file, and animated by libadwaita. Everything in
More GTK 4 — list models, sorters, filters — is built on
properties, and none of it works on a plain attribute.
This chapter is the fifteen minutes that makes the rest of Part I stop feeling arbitrary.
Subclassing
from gi.repository import GObject
class Person(GObject.Object):
__gtype_name__ = "Person"
def __init__(self, name=""):
super().__init__()
self.name = name
Two things are different from an ordinary Python class.
super().__init__() is not optional, and it has to run before anything touches
a property. GObject construction happens on the C side; skipping it leaves a
half-built object that fails later and confusingly.
__gtype_name__ gives the type a name on the C side. Without it PyGObject
invents one from the class name, which is fine until two classes in one process
end up with the same invented name — at which point registration fails outright.
Set it, make it unique, and prefix it with something of your own in a library.
You subclass GTK widgets the same way, which is how
class Window(Adw.ApplicationWindow) works in every other chapter.
Properties
class Person(GObject.Object):
__gtype_name__ = "Person"
name = GObject.Property(type=str, default="")
age = GObject.Property(type=int, default=0, minimum=0, maximum=150)
That is the whole declaration. name and age now read and write like ordinary
attributes:
person.age = 37
print(person.name)
and also by name, which is the part that matters:
person.get_property("name")
person.set_property("age", 37)
Anything generic — a binding, a sorter, a .ui file, an animation — reaches your
data through that second form.
Naming
A property declared as date_of_birth is called date-of-birth by GTK.
Underscores become hyphens at the boundary. Python code uses the underscore
spelling, and everything taking a property name as a string — bind_property,
notify::, Gtk.PropertyExpression — uses hyphens. Both spellings are accepted
in most places; hyphens are what the documentation uses.
Computed and validating properties
For a property that is derived, or that has to check or normalise what it is given, use the decorator form:
@GObject.Property(type=str)
def nickname(self):
return self._nickname
@nickname.setter
def nickname(self, value):
self._nickname = value.strip().title()
A read-only property is one with no setter and the READABLE flag:
@GObject.Property(type=str, flags=GObject.ParamFlags.READABLE)
def summary(self):
return f"{self.name}, aged {self.age}"
A derived property has to be told when its inputs change, or nothing watching it will ever hear:
self.connect("notify::name", lambda *_: self.notify("summary"))
self.connect("notify::age", lambda *_: self.notify("summary"))
Watching for changes
Every property emits notify::<name> when it changes:
person.connect("notify::age", lambda obj, pspec: print(obj.age))
The handler takes the object and a GParamSpec describing the property. You will
ignore the second argument almost every time, and it still has to be in the
signature.
This is the same mechanism as the notify::selected, notify::active and
notify::timestamp handlers in earlier chapters. It is not a special case for
widgets — it is how every GObject reports change.
How they fail
The two failure modes are not symmetrical, and one of them is easy to miss:
person.age = 300 # out of range
person.age = "thirty" # wrong type
Out of range warns and leaves the old value in place. It is not clamped, and
nothing is raised — you get a Warning on stderr and person.age is still
whatever it was. In a program that already prints warnings, this is invisible.
The wrong type raises TypeError, because the conversion cannot even be
attempted.
So a range on a property is a good assertion and a bad validator. If a value coming from outside your program has to be inside a range, check it yourself before assigning.
The full example is examples/gtk4/gobject/properties.py.
Signals
Widgets emit signals; so can your own objects. A custom signal is right whenever a component needs to announce something without knowing who is listening — which keeps a component from having to hold a reference to everything that cares about it.
class Download(GObject.Object):
__gtype_name__ = "Download"
@GObject.Signal(arg_types=(str,))
def started(self, url):
print(f"started {url}")
finished = GObject.Signal()
finished is a signal with no arguments and no body. started takes a string, and
its method body is the signal’s default handler.
download.emit("started", "https://example.com/f")
download.connect("started", lambda _d, url: ...)
The default handler runs first
With no flags, a signal is RUN_FIRST: the method body runs before anything
connected with connect(). That surprises people who expect the class’s own
behaviour to be the fallback. If you want it to be a fallback, say so:
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST)
Return values and vetoes
A signal can return a value, and an accumulator decides what happens when
several handlers each return one. The useful one is
signal_accumulator_true_handled: emission stops at the first handler returning
True, and that becomes the result.
@GObject.Signal(return_type=bool, arg_types=(str,),
flags=GObject.SignalFlags.RUN_LAST,
accumulator=GObject.signal_accumulator_true_handled)
def confirm_overwrite(self, filename):
return False # nobody objected
if self.emit("confirm_overwrite", filename):
return # somebody did
That is the veto pattern, and it is exactly how GTK’s own delete-event-style
signals work: any listener can say “I have handled this, stop”.
With no listeners connected, the default handler’s False stands — so the
sensible behaviour is the one you get for free.
Disconnecting
handler = obj.connect("finished", on_finished)
obj.disconnect(handler)
Worth caring about for one reason: a connected handler holds a reference. If a long-lived object holds a handler that closes over a window, that window cannot be finalised when it closes. The symptom is a program whose memory grows every time a dialog is opened.
The rule of thumb: connecting a widget to itself, or to something it owns, needs no thought. Connecting a short-lived object to a long-lived one means keeping the handler id and disconnecting when the short-lived one goes away.
The full example is examples/gtk4/gobject/signals.py.
Bindings
bind_property() is the most under-used thing in GObject. Any time you would
write “when this changes, set that”, a binding is shorter, cannot get the two out
of sync, and cleans itself up when either object is finalised.
switch.bind_property("active", settings, "enabled",
GObject.BindingFlags.SYNC_CREATE
| GObject.BindingFlags.BIDIRECTIONAL)
The flags are the whole API:
SYNC_CREATE- Copy the current value across immediately. Almost always what you want. Without it the target keeps whatever it had until the source next changes, which is the usual cause of “my binding does not work” on the first frame.
BIDIRECTIONAL- Changes flow both ways.
INVERT_BOOLEAN- For the commonest transform of all.
For anything else, supply a transform function. It returns a (handled, value)
pair, and returning False rejects the value and leaves the target alone:
def to_percent(_binding, value):
return True, f"{value * 100:.0f}%"
settings.bind_property("volume", label, "label",
GObject.BindingFlags.SYNC_CREATE, to_percent)
One property can drive several targets, which is how a single enabled setting
greys out four widgets with no handler at all.
Not everything is a property
A method called get_value() does not imply a property called value. The one
that catches everyone:
scale.bind_property("value", settings, "volume", ...)
# TypeError: Cannot create binding from <Gtk.Scale ...>.value
A Gtk.Scale keeps its value in its Gtk.Adjustment, and that is what the
binding attaches to:
scale.get_adjustment().bind_property("value", settings, "volume", ...)
Similarly Gtk.WebView.can_go_back() in
Embedding Web Content is a method with no property behind
it. When a binding fails with “cannot create binding”, the property does not
exist under that name — check the API reference, or ask the object:
print([p.name for p in obj.list_properties()])
Keeping the binding
bind_property() returns a GObject.Binding. You can ignore it — the binding
lives as long as both objects — but keep it if it has to be taken apart early:
binding = source.bind_property(...)
binding.unbind()
The full example is examples/gtk4/gobject/bindings.py.
Why the list widgets needed all this
Now the shape of More GTK 4 should make sense.
A Gio.ListStore holds GObjects because the machinery around it — sorters,
filters, expressions — reaches values through the property system. A
Gtk.PropertyExpression.new(Package, None, "name") is a compiled instruction to
read the name property, evaluated in C over a million rows without Python being
involved. The bind_property in a list factory’s bind handler keeps a check
button and a data object in step with no callback. And a row updates when the
underlying object changes because the object emits notify.
None of it can see self.name = name. All of it can see
name = GObject.Property(type=str).
Summary
super().__init__()first, and set__gtype_name__.GObject.Propertyis what GTK can see; a plain attribute is not.date_of_birthin Python isdate-of-birthto anything taking a name.- Every property emits
notify::<name>; derived properties must callnotify()themselves. - Out-of-range assignment warns and keeps the old value; the wrong type raises.
- A signal’s default handler runs first unless you ask for
RUN_LAST. signal_accumulator_true_handledgives you a veto.- Keep handler ids when a short-lived object connects to a long-lived one.
bind_propertywithSYNC_CREATEreplaces most “when this changes” handlers.- “Cannot create binding” means the property does not exist under that name.
More GTK 4 is next, and now the list machinery has a reason for being the shape it is.