Table Of Contents
Migrating from Kivy 2.x.x to Kivy 3.x.x¶
Introduction¶
Kivy 3.x.x introduces several changes and improvements compared to Kivy 2.x.x. This guide will help you migrate your existing Kivy 2.x.x codebase to Kivy 3.x.x.
Renamed modules¶
Migration from kivy.core.audio to kivy.core.audio_output
In Kivy 3.x.x, the kivy.core.audio module has been renamed as kivy.core.audio_output.
To migrate your code, you need to update the import statements in your codebase. For example, if you have the following import statement in your code:
from kivy.core.audio import SoundLoader
You need to update it to:
from kivy.core.audio_output import SoundLoader
Removals¶
Removal of `.play` property from `kivy.uix.video.Video` and `kivy.uix.videoplayer.VideoPlayer`
In Kivy 3.x.x, the .play property has been removed from the kivy.uix.video.Video and kivy.uix.videoplayer.VideoPlayer classes.
To migrate your code, you need to update the references to the .play property in your codebase. For example, if you have the following code in your Kivy 2.x.x codebase:
video = Video(source='video.mp4')
# Play the video
video.play = True
# Stop the video
video.play = False
You need to update it to:
video = Video(source='video.mp4')
# Play the video
video.state = 'play'
# Stop the video
video.state = 'stop'
# Pause the video
video.state = 'pause'
Removal of `padding_x` and `padding_y` Properties from `kivy.uix.textinput.TextInput`
In Kivy 3.x.x the padding_x and padding_y properties have been removed from the kivy.uix.textinput.TextInput class. Instead, padding is now managed through the unified padding property.
To update your code, replace instances of padding_x and padding_y with the padding property.
The padding property accepts a list of values, allowing for more flexible padding configurations:
[horizontal, vertical] — e.g., [10, 10]
[padding_left, padding_top, padding_right, padding_bottom] — e.g., [10, 5, 10, 5]
For more details on how to use the padding property, please refer to the related documentation.
Removal of `file_encodings` Property from `kivy.uix.filechooser.FileChooserController`
In Kivy 3.x.x, the file_encodings property has been removed from the kivy.uix.filechooser.FileChooserController class.
The file_encodings property was deprecated and it was kept for backward compatibility, however it was just ignored and not used internally.
To migrate your code, you just need to remove any references to the file_encodings property in your codebase.
ButtonBehavior¶
Removal of `state`, `min_state_time`, `last_touch` Properties and `trigger_action()` Method
In Kivy 3.x.x, the ButtonBehavior class has been significantly simplified and improved. The state OptionProperty, min_state_time NumericProperty, and last_touch ObjectProperty have been removed, along with the trigger_action() method. A simpler, read-only pressed BooleanProperty is now used to indicate the button’s state.
Migrating from `state` to `pressed`
The state property that could be ‘normal’ or ‘down’ has been replaced with a boolean pressed property. Note that pressed is read-only (an AliasProperty), unlike the old state which could be set directly.
# Kivy 2.x.x
if button.state == 'down':
print("Button is pressed")
button.state = 'down' # Could set state directly
# Kivy 3.x.x
if button.pressed:
print("Button is pressed")
# button.pressed = True # NOT ALLOWED - read-only property
In KV language:
# Kivy 2.x.x
Button:
color: (1, 0, 0, 1) if self.state == 'down' else (1, 1, 1, 1)
# Kivy 3.x.x
Button:
color: (1, 0, 0, 1) if self.pressed else (1, 1, 1, 1)
Binding to State Changes
Use the on_pressed property event instead of on_state:
# Kivy 2.x.x
def on_state(self, instance, value):
if value == 'down':
print("Pressed")
else:
print("Released")
# Kivy 3.x.x
def on_pressed(self, instance, is_pressed):
if is_pressed:
print("Pressed")
else:
print("Released")
Migrating from `min_state_time`
The min_state_time property, which enforced a minimum duration for the ‘down’ state, has been removed. If you need similar behavior, you can implement it manually using Clock.create_trigger():
from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.label import Label
class DelayedButton(ButtonBehavior, Label):
MIN_STATE_TIME = 0.5 # seconds
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._release_trigger = Clock.create_trigger(
self._delayed_action,
self.MIN_STATE_TIME
)
self.bind(pressed=self._pressed_changed)
def _pressed_changed(self, instance, is_pressed):
if is_pressed:
self._release_trigger.cancel()
else:
self._release_trigger() # schedule delayed action
def _delayed_action(self, dt):
# Your delayed logic here
print("Action executed after minimum time")
Removal of `trigger_action()` Method
The trigger_action() method has been removed. This method was used to programmatically simulate button presses, but it violated the principle of UI event simulation and wasn’t properly integrated with the touch system.
# Kivy 2.x.x
button.trigger_action(duration=0.1)
If you need to programmatically trigger button actions in Kivy 3.x.x, you have two options:
Option 1: Dispatch events directly (recommended for simple cases)
Simply dispatch the on_press and on_release events without simulating the full touch cycle:
# Kivy 3.x.x - Direct event dispatch
button.dispatch('on_press')
# ... your logic ...
button.dispatch('on_release')
Note that this approach does NOT update the pressed property or trigger internal state changes, as those are tied to actual touch events.
Option 2: Simulate touch events (for complete button state simulation)
If you need the button to fully simulate touch behavior (including updating the pressed property), you must simulate actual touch events:
from kivy.input.motionevent import MotionEvent
class MyButton(ButtonBehavior, Label):
def simulate_press(self, duration=0.1):
"""Simulate a complete button press with touch events."""
# Create a mock touch event
touch = MotionEvent('mock', 0, {
'x': self.center_x,
'y': self.center_y,
'pos': (self.center_x, self.center_y)
})
# Simulate touch down
self.on_touch_down(touch)
# Simulate touch up after duration
def release_touch(dt):
self.on_touch_up(touch)
if duration > 0:
Clock.schedule_once(release_touch, duration)
else:
release_touch(0)
Or create a helper method in your custom button class:
from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.label import Label
class MyButton(ButtonBehavior, Label):
def trigger_action(self, duration=0.1):
"""Simulate button press/release."""
self._do_press()
self.dispatch('on_press')
def trigger_release(dt):
self._do_release()
self.dispatch('on_release')
if not duration:
trigger_release(0)
else:
Clock.schedule_once(trigger_release, duration)
Removal of `last_touch` Property
The last_touch ObjectProperty has been removed. If you need to track touches, implement your own tracking:
# Kivy 2.x.x
class MyButton(ButtonBehavior, Label):
def on_press(self):
print(f"Touch at: {self.last_touch.pos}")
# Kivy 3.x.x
class MyButton(ButtonBehavior, Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.last_touch = None
def on_touch_down(self, touch):
result = super().on_touch_down(touch)
if result and self in touch.ud:
self.last_touch = touch
return result
def on_press(self):
if self.last_touch:
print(f"Touch at: {self.last_touch.pos}")
Improved Multi-Touch Behavior
The on_release event behavior has changed for multi-touch scenarios:
Kivy 2.x.x: on_release fires when the first touch is released (even if other touches are still active)
Kivy 3.x.x: on_release fires only after all active touches are released
# Example: Multi-touch behavior difference
class MyButton(ButtonBehavior, Label):
def on_release(self):
print("Button released")
# Scenario: User presses button with 3 fingers, then lifts them one by one
# Kivy 2.x.x: "Button released" prints when the FIRST finger is lifted
# Kivy 3.x.x: "Button released" prints only when ALL 3 fingers are lifted
If you need the old behavior (release on first touch up), override on_touch_up:
class LegacyButton(ButtonBehavior, Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._first_touch_released = False
def on_touch_down(self, touch):
result = super().on_touch_down(touch)
if result:
self._first_touch_released = False
return result
def on_touch_up(self, touch):
if touch.grab_current is self and not self._first_touch_released:
self._first_touch_released = True
self._do_release()
self.dispatch('on_release')
return super().on_touch_up(touch)
New `on_cancel` Event
A new event on_cancel is now dispatched when a touch moves outside the button bounds during a drag operation. This only occurs when always_release=False (the default).
class MyButton(ButtonBehavior, Label):
def on_press(self):
self.color = (1, 0, 0, 1) # Red when pressed
def on_release(self):
self.color = (0, 1, 0, 1) # Green on successful release
print("Button action executed")
def on_cancel(self):
self.color = (1, 1, 1, 1) # White when cancelled
print("Button action cancelled")
This event allows you to provide visual feedback when the user drags their finger/pointer outside the button, indicating the action will not be executed.
Changed `always_release` Behavior
The behavior when always_release=False (default) has been improved:
Kivy 2.x.x: When touch moved outside bounds, on_release didn’t fire. However, if the user moved the touch back inside the button bounds before releasing, on_release would fire normally. This could cause unexpected side effects.
Kivy 3.x.x: When touch moves outside bounds during drag: - on_cancel event fires immediately (NEW) - Touch is marked as cancelled permanently - on_release will NOT fire, even if touch moves back inside before release - Provides explicit feedback about cancellation - Now, canceled on_release are explicitly canceled on_release`(`on_cancel).
# Example: Standard button behavior (always_release=False)
class StandardButton(ButtonBehavior, Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.always_release = False # Default, but explicit here
def on_press(self):
print("Action started")
self.text = "Release here to confirm"
def on_release(self):
print("Action confirmed!")
self.text = "Confirmed"
def on_cancel(self):
print("Action cancelled")
self.text = "Cancelled - press again"
When always_release=True, the behavior remains the same - on_release always fires and on_cancel never fires:
# Example: Always release (drag-and-drop style)
class DragButton(ButtonBehavior, Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.always_release = True # Release fires anywhere
def on_release(self):
print("Released - on_cancel never fires")
Internal Hooks for Subclassing
The methods _do_press(), _do_release(), and _do_cancel() are now documented as internal hooks for subclasses (like ToggleButtonBehavior). These are called before the corresponding public events are dispatched.
Note
Avoid using these methods, they are for internal state management in subclasses. Application code should use the on_press, on_release, and on_cancel events instead.
class CustomButton(ButtonBehavior, Label):
def _do_press(self):
# Internal state changes before event dispatch (for internal use only)
super()._do_press()
self._internal_state = "pressing"
def on_press(self):
# Public event handler for application logic
print("Button pressed - use this for your logic")
Summary of Breaking Changes
Removed/Changed |
Kivy 2.x.x |
Kivy 3.x.x |
|---|---|---|
state property |
‘normal’ or ‘down’ |
Removed - use pressed (read-only) |
min_state_time |
NumericProperty (0.035) |
Removed - implement manually |
last_touch |
ObjectProperty |
Removed - track manually |
trigger_action() |
Method available |
Removed - dispatch events manually |
on_release behavior |
Fires on first touch up |
Fires after all touches released |
on_cancel event |
Not available |
New - fires on drag outside bounds |
always_release=False |
Silent non-release |
Explicit on_cancel event |
Internal hooks |
Undocumented |
Documented _do_*() methods |
ToggleButtonBehavior¶
Replacement of `state` with `active` and Major API Improvements
In Kivy 3.x.x, ToggleButtonBehavior has undergone significant improvements and changes. The most notable change is replacing the state OptionProperty with an active boolean property (AliasProperty), along with new features like scoped groups and the toggle_on property.
Migrating from `state` to `active`
The state property (‘normal’ or ‘down’) has been replaced with a boolean active property:
# Kivy 2.x.x
if toggle.state == 'down':
print("Toggle is active")
toggle.state = 'normal' # Deactivate
# Kivy 3.x.x
if toggle.active:
print("Toggle is active")
toggle.active = False # Deactivate
In KV language:
# Kivy 2.x.x
ToggleButton:
text: "ON" if self.state == 'down' else "OFF"
color: (0, 1, 0, 1) if self.state == 'down' else (1, 1, 1, 1)
# Kivy 3.x.x
ToggleButton:
text: "ON" if self.active else "OFF"
color: (0, 1, 0, 1) if self.active else (1, 1, 1, 1)
New `on_active` Event
Replace on_state bindings with on_active:
# Kivy 2.x.x
class MyToggle(ToggleButtonBehavior, Label):
def on_state(self, instance, value):
if value == 'down':
print("Activated")
self.background_color = (0, 1, 0, 1)
else:
print("Deactivated")
self.background_color = (1, 1, 1, 1)
# Kivy 3.x.x
class MyToggle(ToggleButtonBehavior, Label):
def on_active(self, instance, value):
if value:
print("Activated")
self.color = (0, 1, 0, 1)
else:
print("Deactivated")
self.color = (1, 1, 1, 1)
In KV language, bind to property changes:
# Kivy 2.x.x
<MyToggle@ToggleButton>:
on_state: app.handle_toggle(self, self.state)
# Kivy 3.x.x
<MyToggle@ToggleButton>:
on_active: app.handle_toggle(self, self.active)
New `toggle_on` Property
A new toggle_on property controls when the toggle state changes - either on press or on release (default):
# Kivy 3.x.x - Toggle immediately on press
ToggleButton:
toggle_on: 'press'
# Kivy 3.x.x - Toggle on release (default)
ToggleButton:
toggle_on: 'release'
This is useful when you want instant visual feedback:
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.uix.label import Label
class InstantToggle(ToggleButtonBehavior, Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.toggle_on = 'press' # Toggle immediately
def on_active(self, instance, value):
self.text = "ON" if value else "OFF"
Scoped Groups (New Tuple Syntax)
Groups can now be scoped to specific widget owners using tuple syntax. This prevents conflicts when creating reusable components:
# Kivy 2.x.x - Only global groups (string)
ToggleButton:
group: 'options' # Global across entire application
# Kivy 3.x.x - Global groups (still supported)
ToggleButton:
group: 'options'
# Kivy 3.x.x - Scoped groups (NEW)
ToggleButton:
group: (root, 'options') # Scoped to 'root' widget
Scoped Group Format: (owner, name)
owner: Any object to scope the group to (typically a widget)
name: Hashable identifier (string, int, enum, etc.)
Example of reusable component with scoped groups:
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.togglebutton import ToggleButton
class FilterPanel(BoxLayout):
"""Reusable panel with independent toggle groups."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Each FilterPanel instance has its own "size" group
for size in ["Small", "Medium", "Large"]:
btn = ToggleButton(
text=size,
group=(self, "size"), # Scoped to this FilterPanel instance
allow_no_selection=False
)
self.add_widget(btn)
# Multiple panels won't interfere with each other
panel1 = FilterPanel() # Has independent "size" group
panel2 = FilterPanel() # Has independent "size" group
In KV language:
<FilterPanel@BoxLayout>:
ToggleButton:
text: "Small"
group: (root, "size") # Scoped to FilterPanel instance
ToggleButton:
text: "Medium"
group: (root, "size")
ToggleButton:
text: "Large"
group: (root, "size")
# Each instance has independent groups
BoxLayout:
FilterPanel: # Independent "size" group
FilterPanel: # Independent "size" group
When to Use Scoped vs Global Groups
Use global groups (string) when: - You want all toggles across the app to share the same group - Building simple single-instance interfaces
Use scoped groups (tuple) when: - Creating reusable components with internal toggle groups - Multiple instances of the same component shouldn’t interfere - Building complex layouts with nested toggle groups
Method Changes: `get_widgets()` → `get_group()`
The static method get_widgets(groupname) has been replaced with an instance method get_group():
# Kivy 2.x.x - Static method
widgets = ToggleButtonBehavior.get_widgets('mygroup')
for widget in widgets:
print(widget.text)
del widgets # Always delete to prevent memory leaks
# Kivy 3.x.x - Instance method
widgets = my_toggle_button.get_group()
for widget in widgets:
print(widget.text)
del widgets # Still recommended to delete to prevent memory leaks
The new get_group() method: - Returns widgets in the same group as the calling instance - Works with both global and scoped groups automatically - Includes the calling widget in the returned list
# Example usage
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.togglebutton import ToggleButton
class MyApp(App):
def build(self):
layout = BoxLayout()
btn1 = ToggleButton(text="Option 1", group="options")
btn2 = ToggleButton(text="Option 2", group="options")
btn3 = ToggleButton(text="Option 3", group="options")
layout.add_widget(btn1)
layout.add_widget(btn2)
layout.add_widget(btn3)
# Get all widgets in btn1's group
group_widgets = btn1.get_group()
print(f"Group has {len(group_widgets)} widgets") # Prints: 3
del group_widgets
return layout
Removal of `_clear_groups()` and `_release_group()`
The static method _clear_groups() and instance method _release_group() have been removed. Group management is now handled automatically through weak references.
# Kivy 2.x.x - Manual group management
class MyToggle(ToggleButtonBehavior, Label):
def custom_release(self):
self._release_group(self)
self.state = 'normal'
# Kivy 3.x.x - Automatic group management
class MyToggle(ToggleButtonBehavior, Label):
def custom_release(self):
self.active = False # Automatically manages group
Improved Group Cleanup
Group management now uses WeakSet for automatic cleanup when widgets are deleted. You no longer need to manually clean up groups:
# Kivy 2.x.x - Potential memory leaks if not careful
toggle = ToggleButton(group='mygroup')
del toggle # Weak reference might linger
# Kivy 3.x.x - Automatic cleanup
toggle = ToggleButton(group='mygroup')
del toggle # Automatically removed from group via WeakSet
Summary of Breaking Changes
Removed/Changed |
Kivy 2.x.x |
Kivy 3.x.x |
|---|---|---|
state property |
‘normal’ or ‘down’ |
Replaced with active (bool) |
on_state event |
Fired on state change |
Replaced with on_active |
toggle_on property |
Not available |
New - ‘press’ or ‘release’ |
Group syntax |
String only |
String or (owner, name) tuple |
get_widgets(group) |
Static method |
Replaced with get_group() instance |
_clear_groups() |
Static method |
Removed - automatic cleanup |
_release_group() |
Instance method |
Removed - automatic via active |
Group management |
Manual weak references |
Automatic with WeakSet |