GObject Introspection
GObject Introspection (GI) is the mechanism that generates the Python bindings for the GTK 3 (and GTK 4) stack at runtime - the layer that makes from gi.repository import Gtk possible. Where PyGTK was a hand-maintained binding (the pygobject code generated for GTK 2's API), GI reads the API's metadata (the .gir files, compiled to the .typelib binaries) and builds the Python classes, methods, and signals from that metadata when the module is imported. The model is the GType system (the C type registry every GObject-based library uses), the .gir/.typelib as the API description, and the runtime generation that turns the description into the Python objects the app uses. This chapter covers the GType and the signal system GI builds on, the .gir/.typelib and the require_version, the runtime generation, the properties and the signals as the object model, and what the mechanism means for the app (the binding is the library's, not a separate project's).
GType: The Type System GI Builds On
# the GType is the C type registry - every GObject-based
# library (GLib, GDK, GTK, GStreamer, ...) registers its
# types in it, and the type is the runtime identity of the
# object's class
# in the Python binding, the GType is invisible but present:
# the Gtk.Window is a Python class generated FOR the
# G_TYPE_WINDOW, and the isinstance / the type checks work
# through that registry
# the object model GI generates is the GObject model:
# the type (the GType - the class identity)
# the properties (the get/set, the validation, the
# notification on change)
# the signals (the named events, the connect/disconnect)
# the interfaces (the GInterface the type implements)
# the property - the GObject's get/set with the notification:
from gi.repository import Gtk
button = Gtk.Button()
button.set_property("label", "Save") # the set
print(button.get_property("label")) # the get
# the property change emits the "notify::label" signal -
# the observation of the property is a signal connection:
button.connect("notify::label", on_label_changed)
# the signal - the named event, the same system PyGTK used:
button.connect("clicked", on_clicked)
# the connect/disconnect, the handler id, the stop_emission
# - the signal SYSTEM is the GObject's, shared by every
# GObject-based library, and GI exposes it uniformly
The GType is the C type registry that every GObject-based library registers its types in - the runtime identity of the class, the foundation the whole object model stands on. In the Python binding the GType is invisible (the Gtk.Window is the Python class generated for the G_TYPE_WINDOW), but it is what makes the type system work across the libraries (the isinstance, the downcast, the interface check all resolve through it). The object model GI generates from the GType is the GObject model with its four parts: the type (the class identity), the properties (the get_property/set_property with the validation and the notify:: signal on change - the observation of a property is a signal connection, the notify::label), the signals (the named events with the connect/disconnect, the handler id, the stop_emission - the same signal system PyGTK used, because it is the GObject's, shared by every GObject-based library), and the interfaces (the GInterface the type implements). The point of the mechanism: the object model is uniform across the libraries, because they all stand on the same GType and the same signal and property system, and GI exposes that uniformity to Python.
The .gir and the .typelib: The API Description
# the introspection data - two artifacts per library version:
# 1. the .gir - the GObject Introspection REpresentation:
# the XML description of the API - the namespaces, the
# classes, the methods (the signatures), the properties,
# the signals, the constants. the SOURCE of the binding.
# (libgtk-3.0.gir)
# 2. the .typelib - the compiled, binary form of the .gir:
# what the runtime LOADS. the .gir is compiled to the
# .typelib by the introspection compiler (when the
# library is built with introspection enabled).
# (Gtk-3.0.typelib)
# the app meets the .typelib, not the .gir: the gi module
# loads the typelib for the requested namespace + version
# and generates the Python classes from it at import time
# the require_version selects the typelib:
# gi.require_version("Gtk", "3.0") -> loads Gtk-3.0.typelib
# the same namespace at another version (the "4.0") is a
# different typelib - the version is part of the identity
# the typelib location: the GI_TYPELIB_PATH (the default is
# the distro's /usr/lib/.../girepository-1.0) - the app can
# add a path for a private or a development typelib
The introspection data is the two artifacts: the .gir (the XML description of the API - the namespaces, the classes, the method signatures, the properties, the signals, the constants - the source) and the .typelib (the compiled binary form the runtime loads). The app meets the .typelib: the gi module loads the typelib for the requested namespace and version and generates the Python classes from it at import time, which is the "runtime generation" - the binding is not a compiled Python extension built against the library, it is the metadata read at import and the classes constructed from it. The require_version is the selector: the gi.require_version("Gtk", "3.0") loads the Gtk-3.0.typelib, and the same namespace at another version is a different typelib (the version is part of the identity, which is why the require_version must precede the import). The GI_TYPELIB_PATH is the location (the distro's girepository-1.0 directory, extendable for a private or a development typelib). The mechanism is the one that makes the binding the library's: the library ships its API description, and the binding is generated from it, so a new library version with introspection enabled is a new typelib and a working binding, with no separate binding project to release.
The Runtime Generation: What the Import Does
import gi
# the import sequence - what the runtime does:
gi.require_version("Gtk", "3.0")
# 1. records the requested version for the "Gtk" namespace
from gi.repository import Gtk
# 2. loads the Gtk-3.0.typelib
# 3. walks the description: for each class, property, signal,
# and constant, constructs the corresponding Python object:
# - the class (a GObject subclass with the methods)
# - the methods (the wrappers that call the C functions)
# - the properties (the get/set, the notify wiring)
# - the signals (the connect/disconnect, the emission)
# - the constants (the enum values, the flags)
# 4. registers the module in gi.repository
# the result: the Python module IS the generated binding -
# every attribute the app uses (Gtk.Window, the .connect,
# the .set_property, the Gtk.ResponseType.OK) was constructed
# from the typelib at this import
# the consequence: the binding's fidelity is the typelib's -
# a method in the library that the .gir describes is in the
# Python module; the one it does not (the C-only, the
# non-introspectable) is the gap the app meets as the
# "attribute not found" (the C function with no GI wrapper)
# the introspectable surface is the library's documented API
# - the GI metadata is generated from the library's headers
# with the introspection annotations, and it is the API the
# binding exposes
The import is the generation: the require_version records the requested version, the from gi.repository import Gtk loads the Gtk-3.0.typelib, and the runtime walks the description - for each class, property, signal, and constant, it constructs the corresponding Python object (the class as a GObject subclass with its methods, the method wrappers that call the C functions, the properties with the notify wiring, the signals with the connect/disconnect and the emission, the constants as the enum and flag values) - and registers the module. The result is that the Python module is the generated binding: every attribute the app uses was constructed from the typelib at the import, and the binding's fidelity is the typelib's - a method the .gir describes is in the module, and the C-only function with no introspection annotation is the gap the app meets as the missing attribute. The introspectable surface is the library's documented API, because the GI metadata is generated from the library's headers with the introspection annotations. The mechanism is what makes the binding track the library: the library's API description is the binding's source, and the generation at import is the bridge from the C API to the Python object the app calls.
The Object Model: Properties, Signals, the App's View
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib
# the property as the object model's data:
entry = Gtk.Entry()
entry.set_property("text", "hello")
# the "notify::text" - the observation of the change:
def on_text_changed(obj, param):
# param is the GParamSpec - param.name is "text"
print("text is now:", obj.get_property("text"))
entry.connect("notify::text", on_text_changed)
entry.set_property("text", "changed") # fires the notify
# the signal as the object model's events:
# the same system, the same connect, the same handler id:
hid = entry.connect("activate", on_activate)
entry.disconnect(hid)
# the app's view of the model: the app codes against the
# GENERATED surface - the classes, the properties, the
# signals, the constants - and that surface is the library's
# API as described by the typelib. the app does not know (and
# does not need to) that the binding was generated at import;
# it uses the objects as if they were hand-written, because
# to the app they ARE the module's objects
# the uniformity: the same property/signal model for Gtk,
# for Gdk, for GLib, for GStreamer, for every GObject library
# - the introspection is the mechanism, the GObject model is
# the vocabulary, and the app's code is the same shape across
# the libraries
The object model is what the app codes against, and it is the GObject model in its uniform form. The property is the data - the set_property/get_property, with the notify::property signal as the observation (the GParamSpec in the handler naming the property that changed) - and the signal is the event - the same connect/disconnect, the handler id, the emission, the system PyGTK used because it is the GObject's. The app's view is the generated surface: the classes, the properties, the signals, the constants, used as if they were hand-written (because to the app they are the module's objects), and the app does not need to know the binding was generated at the import. The uniformity is the point of the mechanism: the same property and signal model across Gtk, Gdk, GLib, GStreamer, and every GObject-based library, because they all stand on the GType and the GObject model, and the introspection exposes that model to Python uniformly. The app's code is the same shape across the libraries - the connect, the set_property, the notify - which is the consistency the generated binding provides and the hand-maintained binding had to maintain by hand.
The Working Rules
- The
require_versionprecedes the import. It selects the typelib (the namespace plus the version is the identity), and the import without it resolves the default - the explicit version is the app's declaration of the toolkit it binds. - The binding is the library's, generated at import. The
.typelibis the API description, the runtime generates the Python objects from it, and a new library version with introspection is a new typelib and a working binding - no separate binding project. - The introspectable surface is the documented API. A method the
.girdescribes is in the module; the C-only function is the missing attribute - the gap is the annotation, not the binding's failure. - The property and the signal are the object model. The
set_property/get_propertywith thenotify::observation, theconnect/disconnectwith the handler id - the uniform vocabulary across every GObject library the app uses. - Code against the generated surface as if it were hand-written. The app does not know or need to know the generation happened at the import; the objects are the module's, and the code's shape is the same across the libraries.
Summary
GObject Introspection is the runtime binding generator: the .gir (the XML API description) compiled to the .typelib (the binary the runtime loads), the gi.require_version(namespace, version) selecting the typelib, and the from gi.repository import Gtk that loads it and constructs the Python classes, methods, properties, signals, and constants from the description at import time. The foundation is the GType registry (the class identity every GObject library shares) and the GObject object model - the properties (the get/set with the notify:: observation), the signals (the connect/disconnect, the handler id, the emission), the interfaces - which is the same model PyGTK exposed, because it is the GObject's, shared uniformly across the libraries. The consequence is the binding as the library's: the API description is the binding's source, the generation tracks the library's version, and the app codes against the generated surface as if it were hand-written, in the same shape across Gtk, Gdk, GLib, and every other GObject-based library it uses.