Skillversitas

Properties and Binding

Kivy's properties are the reactive core: a declared property is a value with a type, a default, and - the important part - the ability to notify when it changes, so that anything bound to it updates automatically. Binding is what makes a Kivy UI feel alive: the label's text bound to a field's text stays in sync without a manual refresh, and a progress bar's value bound to a download's progress fills itself. This chapter covers declaring custom properties, the << binding operator in KV, bind in Python, and the one-way discipline that keeps reactive UIs predictable.

Declaring Properties

from kivy.uix.widget import Widget
from kivy.properties import (StringProperty, NumericProperty,
                              BooleanProperty, ObjectProperty,
                              ListProperty, AliasProperty)

class Recipe(Widget):
    name = StringProperty("")
    servings = NumericProperty(2)
    is_favorite = BooleanProperty(False)
    notes = StringProperty("")
    # a property that holds another widget or object
    source_widget = ObjectProperty(None)
    tags = ListProperty([])

    # a computed property: a value derived from others, with its own events
    def _get_summary(self):
        return f"{self.name} (serves {self.servings})"

    summary = AliasProperty(_get_summary,
                            bind=("name", "servings"))

r = Recipe()
r.bind(on_summary_change=lambda inst, v: print("now:", v))
r.name = "Tomato Soup"
print(r.summary)   # Tomato Soup (serves 2)

The property classes type the value and give it a default: StringProperty, NumericProperty, BooleanProperty, ObjectProperty (any object, including widgets), ListProperty, and the numeric/option families (BoundedNumericProperty for a range, ColorProperty for an RGBA). The power is the change notification: setting r.name fires on_name_change, and an AliasProperty (shown) is a read-only derived property that re-fires whenever any of its bind tuple changes - summary updates because name or servings did, with no code in between. Declaring a property (rather than a plain attribute) is what makes it bindable; a plain self.x = 1 in __init__ changes silently and notifies nothing.

Binding in KV: the << Operator

<RecipePanel>:
    BoxLayout:
        orientation: "vertical"
        TextField:
            id: name_field
            text: "Tomato Soup"
        Label:
            # the << operator: a LIVE binding, not a one-time copy
            text: "Name: " + << name_field.text
        Label:
            # bind to a property on the panel itself (root)
            text: << root.summary

The << (and the ->> reverse) operators in a KV property value create a live binding: the target property updates whenever the source changes, automatically. text: "Name: " + << name_field.text means "this label's text is always 'Name: ' plus the field's current text" - type in the field and the label follows, with no handler. This is the KV idiom for the display-follows-input pattern, and it is the difference from the plain reference (text: name_field.text), which copies once at build time. The reverse operator ->> binds the other direction (the label drives the field). Use the live operator for the display mirrors and the plain reference for a one-time initial value - mixing them up is the "why does my label not update" (or the opposite, an unwanted feedback loop) cause.

Binding in Python: bind and Event

from kivy.properties import NumericProperty
from kivy.uix.progressbar import ProgressBar

class Downloader:
    progress = NumericProperty(0)   # 0..1, updated as the download goes

class Panel:
    def __init__(self):
        self.dl = Downloader()
        self.bar = ProgressBar(max_value=1)
        # bind the bar's value to the downloader's progress
        self.dl.progress.bind(self._on_progress)
        # or the one-liner:
        # self.bar.bind(value=self.dl.progress)  # source -> target

    def _on_progress(self, *args):
        # args = (sender, value)
        self.bar.value = args[1]

    def stop(self):
        self.dl.progress.unbind(self._on_progress)  # the cleanup

The Python side is source_property.bind(callback), where the callback receives (sender, value). The ProgressBar bound to the progress property fills as the value changes - the standard progress pattern. The same bind works on any property of any widget (button.bind(color=self._on_color)), which is how you build cross-widget reactions in code when the KV << is not the right shape (a transformation in between, a condition, a side effect). Unbinding (unbind) is the cleanup that mirrors the event chapter's rule: when the source or the target goes away, remove the binding or the callback fires into a dead object. The discipline is the same one-way rule as the KV operator: bind source-to-target deliberately, and never create a cycle where A drives B and B drives A.

The on_<prop>_change Convention

from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty

class ConfigPanel(BoxLayout):
    theme = StringProperty("light")

    # defining on_<prop>_change makes it the AUTO handler for that property
    def on_theme_change(self, instance, value):
        print("theme is now", value)
        # apply the side effect: restyle, reload, etc.

p = ConfigPanel()
p.theme = "dark"     # fires on_theme_change automatically - no bind needed

A method named on_<prop>_change on the class is automatically bound to that property's changes - no bind call required. It is the idiomatic place for a property's side effect: when theme changes, restyle; when selected changes, load the detail. The convention keeps the reaction next to the property it reacts to, and it is the Python equivalent of the KV << for "this property's change triggers this logic." The distinction from bind: on_<prop>_change is for the class's own reaction to its own property, while bind is for wiring two different objects together. Use the named handler for internal side effects, bind for cross-object bindings, and the KV operator for the declarative display mirrors.

The Working Rules

  • Declare, don't assign. A bindable value is a declared property; a plain attribute change notifies nothing.
  • One direction per binding. Source drives target; a two-way cycle (A binds B, B binds A) is a feedback loop that oscillates or crashes.
  • Use the right tool per shape. KV << for declarative display mirrors, bind for cross-object reactions in code, on_<prop>_change for a class's own side effect, AliasProperty for a derived value.
  • Unbind on teardown. A binding outlives its widgets unless you remove it; the cleanup is part of the binding, not an afterthought.
  • Keep the property change cheap. A property change fires every bound handler synchronously; heavy work in a handler stutters the UI that triggered it.

Summary

Properties are typed, default-valued state that notifies on change, and binding is the propagation of that change to dependents. Declare with the property classes (StringProperty, NumericProperty, ObjectProperty, ...), derive with AliasProperty bound to its inputs, and connect three ways: the KV << operator for live display mirrors, source.bind(callback) in Python for cross-object reactions (with the matching unbind), and the on_<prop>_change method for a class's own side effect. The discipline is directionality - one source, many targets, never a cycle - and cheapness, because a change fires its handlers synchronously on the UI thread. Master the three binding tools and the one-way rule, and the reactive layer - the thing that makes a Kivy UI update itself - is under your control instead of a mystery.