macOS Tutorial Guide

Python GUI Dev:
Kivy & wxPython

A learn-as-you-go tutorial for building desktop & mobile GUI apps on your Mac — from zero to functional.

LESSON 01

What is Kivy?

10% complete
🐍

Cross-Platform GUI & Touch Framework

Kivy is a free, open-source Python library for developing multi-touch applications. It runs on macOS, Windows, Linux, Android, and iOS — write once, deploy everywhere.

Kivy is not a "native" UI toolkit (its widgets look the same on every platform, rendered via OpenGL ES 2). This makes it ideal for apps that need a custom look, games, or mobile targets. It's less suited to apps that must blend into the OS's native aesthetic.

FeatureKivy
TargetsmacOS, Windows, Linux, Android, iOS
RenderingOpenGL ES 2 (GPU accelerated)
Touch/GestureFirst-class support
LicenseMIT
Layout languageKV Language (declarative)
💡 Best For

Custom-styled apps, games, cross-platform mobile/desktop apps, touch-based UIs.


LESSON 02

Installing Kivy on macOS

20% complete

Kivy on macOS works best inside a virtual environment. Follow these steps:

  1. Install Python 3.11+ from python.org (or via Homebrew: brew install python).
  2. Create and activate a virtual environment:
    python3 -m venv kivyenv && source kivyenv/bin/activate
  3. Upgrade pip: pip install --upgrade pip
  4. Install Kivy with all dependencies:
    pip install "kivy[base]"
  5. Verify: python -c "import kivy; print(kivy.__version__)"
⚠️ macOS Note

On Apple Silicon (M1/M2/M3) Macs, make sure your Python is the ARM-native version from python.org — not the Rosetta one — to avoid OpenGL issues.

Optional: Install SDL2 via Homebrew

If you encounter display errors, install the system SDL2 library first:

Terminal
brew install sdl2 sdl2_image sdl2_ttf sdl2_mixer

LESSON 03

Hello, Kivy World!

30% complete

Every Kivy app inherits from App and implements a build() method that returns the root widget.

Python — hello.py
from kivy.app import App
from kivy.uix.label import Label

class HelloApp(App):
    def build(self):
        # Return the root widget — a simple Label
        return Label(text='Hello, Kivy on macOS!')

if __name__ == '__main__':
    HelloApp().run()

Run it from your terminal (with the venv active):

Terminal
python hello.py

A black window with white centred text will appear — you've written your first Kivy app!

💡 Class Naming

Kivy strips "App" from your class name to produce the window title. HelloApp → title becomes "Hello".


LESSON 04

Widgets — The Building Blocks

40% complete

Everything visible in a Kivy app is a Widget. Common ones include:

WidgetImport pathPurpose
Labelkivy.uix.labelDisplay text
Buttonkivy.uix.buttonClickable button
TextInputkivy.uix.textinputUser text entry
Imagekivy.uix.imageDisplay images
CheckBoxkivy.uix.checkboxToggle checkbox
Sliderkivy.uix.sliderValue slider
ToggleButtonkivy.uix.togglebuttonOn/off toggle
Python — button example
from kivy.app import App
from kivy.uix.button import Button

class BtnApp(App):
    def build(self):
        btn = Button(
            text='Click me!',
            font_size=24,
            background_color=(0.2, 0.6, 1, 1)  # R,G,B,A floats
        )
        btn.bind(on_press=self.on_btn_press)
        return btn

    def on_btn_press(self, instance):
        instance.text = 'Pressed!'

BtnApp().run()

LESSON 05

Layouts — Arranging Widgets

50% complete

Widgets need a Layout container to position them on screen. Kivy ships with several:

LayoutBehaviour
BoxLayoutStack widgets horizontally or vertically
GridLayoutFixed rows × columns grid
FloatLayoutAbsolute or relative position/size
AnchorLayoutAnchor child to a corner or edge
StackLayoutLike CSS flexbox wrap
Python — BoxLayout
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label

class LayoutApp(App):
    def build(self):
        layout = BoxLayout(orientation='vertical', padding=20, spacing=10)
        layout.add_widget(Label(text='Name:', size_hint_y=None, height=40))
        layout.add_widget(Button(text='Submit'))
        return layout

LayoutApp().run()
ℹ️ size_hint

size_hint is a 0–1 fraction of the parent's size. Set size_hint_y=None and height=40 for a fixed-pixel height widget.


LESSON 06

KV Language — Declarative UI

60% complete

Defining widgets in Python quickly becomes verbose. KV Language is Kivy's built-in declarative language — like YAML meets CSS for your UI structure.

Create myapp.kv alongside your Python file (Kivy loads it automatically based on your App class name):

KV — myapp.kv
BoxLayout:
    orientation: 'vertical'
    padding: 20
    spacing: 12

    Label:
        text: 'Enter your name:'
        font_size: 18

    TextInput:
        id: name_input
        hint_text: 'Type here...'
        size_hint_y: None
        height: 44

    Button:
        text: 'Greet'
        size_hint_y: None
        height: 50
        on_press: app.greet(name_input.text)
Python — myapp.py
from kivy.app import App

class MyApp(App):
    def greet(self, name):
        print(f'Hello, {name}!')

MyApp().run()
💡 Pro Tip

You can also embed KV strings directly with Builder.load_string() — great for single-file apps.


LESSON 07

Events & Property Binding

70% complete

Kivy uses a reactive property system. When a property changes, bound callbacks fire automatically — no manual event listeners needed in most cases.

Python — reactive property
from kivy.app import App
from kivy.uix.slider import Slider
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout

class SliderApp(App):
    def build(self):
        box = BoxLayout(orientation='vertical', padding=30)
        self.label = Label(text='Value: 50', font_size=22)
        slider = Slider(min=0, max=100, value=50)
        # Bind the 'value' property — fires on any change
        slider.bind(value=self.on_slider_change)
        box.add_widget(self.label)
        box.add_widget(slider)
        return box

    def on_slider_change(self, instance, value):
        self.label.text = f'Value: {value:.0f}'

SliderApp().run()

Common Event Names

EventFired when…
on_pressButton pressed down
on_releaseButton released
on_textTextInput text changes
on_touch_downTouch/click begins on widget
on_valueSlider value changes

LESSON 08

Screen Manager — Multi-Screen Apps

80% complete

Use ScreenManager to build apps with multiple pages (like a mobile app's navigation stack).

Python — screen manager
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition
from kivy.uix.button import Button

class HomeScreen(Screen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        btn = Button(text='Go to Settings →')
        btn.bind(on_press=lambda _: self.manager_goto('settings'))
        self.add_widget(btn)

    def manager_goto(self, screen):
        self.manager.current = screen

class SettingsScreen(Screen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        btn = Button(text='← Back Home')
        btn.bind(on_press=lambda _: setattr(self.manager, 'current', 'home'))
        self.add_widget(btn)

class NavApp(App):
    def build(self):
        sm = ScreenManager(transition=SlideTransition())
        sm.add_widget(HomeScreen(name='home'))
        sm.add_widget(SettingsScreen(name='settings'))
        return sm

NavApp().run()
💡 Transitions

Try FadeTransition, WipeTransition, or NoTransition instead of SlideTransition for different navigation feels.


LESSON 09

Canvas & Custom Drawing

90% complete

Every Kivy widget has a canvas you can draw on using OpenGL-based drawing instructions like Rectangle, Ellipse, Line, and Color.

Python — canvas drawing
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Color, Ellipse, Rectangle

class PaintWidget(Widget):
    def on_touch_down(self, touch):
        with self.canvas:
            Color(0, 0.9, 0.6, 1)  # Kivy green
            d = 30
            Ellipse(pos=(touch.x - d/2, touch.y - d/2), size=(d, d))

class PaintApp(App):
    def build(self):
        return PaintWidget()

PaintApp().run()

Click/tap anywhere in the window and green dots will appear — a mini paint app in 15 lines!


LESSON 10

Capstone — A Temperature Converter

100% complete 🎉

Let's put it all together: layouts, KV language, TextInput, Labels, and event binding into a real, usable app.

Python — converter.py
from kivy.app import App
from kivy.lang import Builder

KV = """
BoxLayout:
    orientation: 'vertical'
    padding: 40
    spacing: 16

    Label:
        text: 'Temperature Converter'
        font_size: 26
        bold: True
        size_hint_y: None
        height: 50

    TextInput:
        id: celsius_input
        hint_text: 'Enter Celsius'
        input_filter: 'float'
        size_hint_y: None
        height: 48

    Button:
        text: 'Convert to Fahrenheit'
        size_hint_y: None
        height: 52
        on_press: app.convert(celsius_input.text)

    Label:
        id: result_label
        text: 'Result will appear here'
        font_size: 20
        color: 0, 0.9, 0.6, 1
"""

class ConverterApp(App):
    def build(self):
        self.root = Builder.load_string(KV)
        return self.root

    def convert(self, value):
        try:
            c = float(value)
            f = c * 9/5 + 32
            self.root.ids.result_label.text = f'{c:.1f}°C  =  {f:.1f}°F'
        except ValueError:
            self.root.ids.result_label.text = 'Please enter a valid number'

ConverterApp().run()
🎓 What you learned

You've installed Kivy, used Widgets, Layouts, KV Language, events, ScreenManager, Canvas drawing, and built a complete app. Next: explore kivy.org docs, Buildozer (for Android packaging), and Kivy Garden for community widgets.

LESSON 01

What is wxPython?

10% complete
🪟

Native Desktop GUI Toolkit

wxPython is a Python wrapper around the wxWidgets C++ library. Unlike Kivy, it renders true native OS controls — your app looks and feels like a real macOS app, using Cocoa widgets under the hood.

wxPython is ideal for desktop-only productivity tools, utilities, file managers, and any application where blending into the OS is important. It's been around since 1996 and remains the gold standard for native-looking Python desktop apps.

FeaturewxPython
TargetsmacOS, Windows, Linux
RenderingNative OS controls (Cocoa on macOS)
Look & FeelMatches the OS perfectly
LicensewxWindows Library Licence
Main packagewxPython Phoenix (wx)
💡 Best For

Desktop utilities, file tools, settings dialogs, productivity apps — anything that should feel truly native on macOS.


LESSON 02

Installing wxPython on macOS

20% complete

wxPython has a pre-built binary (wheel) for macOS — installation is straightforward:

  1. Ensure Python 3.9–3.12 is installed. Check with:
    python3 --version
  2. Create and activate a virtual environment:
    python3 -m venv wxenv && source wxenv/bin/activate
  3. Install wxPython Phoenix (this downloads a ~30MB binary wheel):
    pip install wxpython
  4. If pip fails to find a wheel, install from the official snapshot:
    pip install -U --pre wxPython
  5. Verify: python -c "import wx; print(wx.version())"
⚠️ Apple Silicon Note

On M-series Macs, run your script with arch -arm64 python yourscript.py if you see architecture mismatch errors.

Testing the install

Terminal
python -c "import wx; app = wx.App(); wx.Frame(None, title='wx works!').Show(); app.MainLoop()"

A small native macOS window with the title "wx works!" should appear.


LESSON 03

Hello, wxPython World!

30% complete

Every wxPython app needs three things: a wx.App, a wx.Frame (the window), and a call to MainLoop().

Python — hello.py
import wx

class HelloFrame(wx.Frame):
    def __init__(self):
        super().__init__(
            parent=None,
            title='Hello wxPython on macOS!',
            size=(400, 250)
        )
        panel = wx.Panel(self)
        text = wx.StaticText(
            panel,
            label='Hello, World!',
            style=wx.ALIGN_CENTER
        )
        self.Centre()   # Centre window on screen
        self.Show()

if __name__ == '__main__':
    app = wx.App()
    frame = HelloFrame()
    app.MainLoop()
ℹ️ wx.Panel

Always place widgets inside a wx.Panel within your Frame — it handles keyboard navigation, background repainting, and gives the correct native background colour on macOS.


LESSON 04

Frames & Panels — App Structure

40% complete

Understanding the hierarchy is key to wxPython apps:

ClassRole
wx.AppThe application object — must be created first
wx.FrameA top-level window with title bar, resize handles, etc.
wx.PanelA container widget inside a Frame
wx.DialogA modal dialog window
wx.NotebookTabbed panel container
Python — structured frame
import wx

class MainFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='My App', size=(500, 400))

        # Status bar at the bottom (native on macOS)
        self.CreateStatusBar()
        self.SetStatusText('Ready')

        panel = wx.Panel(self)
        # All widgets go inside panel

        self.Centre()
        self.Show()

wx.App()
MainFrame()
wx.GetApp().MainLoop()

LESSON 05

Controls — Native Widgets

50% complete

wxPython wraps dozens of native macOS (Cocoa) controls. These will automatically look correct — dark mode, system fonts, and all:

WidgetDescription
wx.StaticTextNon-editable text label
wx.TextCtrlSingle or multi-line text input
wx.ButtonPush button
wx.CheckBoxCheckbox with label
wx.RadioButtonGrouped radio buttons
wx.SliderHorizontal/vertical slider
wx.ListBoxScrollable item list
wx.ComboBoxDropdown + text combo
wx.SpinCtrlNumeric up/down spin
Python — various controls
import wx

class ControlsFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Controls Demo', size=(380, 300))
        panel = wx.Panel(self)
        vbox = wx.BoxSizer(wx.VERTICAL)

        text = wx.TextCtrl(panel, value='Enter text here')
        check = wx.CheckBox(panel, label='Enable feature')
        combo = wx.ComboBox(panel, choices=['Option A', 'Option B', 'Option C'])
        btn   = wx.Button(panel, label='Submit')

        for w in [text, check, combo, btn]:
            vbox.Add(w, flag=wx.EXPAND|wx.ALL, border=10)

        panel.SetSizer(vbox)
        self.Centre(); self.Show()

wx.App(); ControlsFrame(); wx.GetApp().MainLoop()

LESSON 06

Sizers — Layout Management

60% complete

wxPython does not use absolute positions. Instead, Sizers manage layout dynamically — they resize and reposition widgets when the window changes size.

SizerBehaviour
wx.BoxSizerStack widgets horizontally or vertically
wx.GridSizerEqual-sized grid of cells
wx.FlexGridSizerGrid with variable row/col sizes
wx.StaticBoxSizerBoxSizer with a labelled border box
Python — FlexGridSizer form
import wx

class FormFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Form', size=(360, 220))
        panel = wx.Panel(self)

        fgs = wx.FlexGridSizer(2, 2, 10, 10)   # rows, cols, vgap, hgap
        fgs.AddGrowableCol(1, 1)               # col 1 stretches

        fields = [('First Name:',), ('Last Name:',)]
        for label, in fields:
            fgs.Add(wx.StaticText(panel, label=label),
                    flag=wx.ALIGN_CENTER_VERTICAL)
            fgs.Add(wx.TextCtrl(panel), flag=wx.EXPAND)

        outer = wx.BoxSizer(wx.VERTICAL)
        outer.Add(fgs, 1, wx.ALL|wx.EXPAND, 20)
        outer.Add(wx.Button(panel, label='Save'),
                  flag=wx.ALIGN_RIGHT|wx.ALL, border=10)
        panel.SetSizer(outer)
        self.Centre(); self.Show()

wx.App(); FormFrame(); wx.GetApp().MainLoop()

LESSON 07

Events — Binding User Actions

70% complete

wxPython uses an event table approach via self.Bind(). You bind an event type to a handler method on any widget.

Python — button events
import wx

class EventFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Events', size=(340, 200))
        panel = wx.Panel(self)
        vbox = wx.BoxSizer(wx.VERTICAL)

        self.label = wx.StaticText(panel, label='Click the button')
        btn = wx.Button(panel, label='Click Me!')
        slider = wx.Slider(panel, value=50, minValue=0, maxValue=100)

        # Bind button click event
        btn.Bind(wx.EVT_BUTTON, self.on_click)
        # Bind slider scroll event
        slider.Bind(wx.EVT_SLIDER, self.on_slide)

        for w in [self.label, btn, slider]:
            vbox.Add(w, flag=wx.ALL|wx.EXPAND, border=14)
        panel.SetSizer(vbox)
        self.Centre(); self.Show()

    def on_click(self, event):
        self.label.SetLabel('Button was clicked!')

    def on_slide(self, event):
        val = event.GetEventObject().GetValue()
        self.label.SetLabel(f'Slider: {val}')

wx.App(); EventFrame(); wx.GetApp().MainLoop()

Common Event Types

EventTriggered by
wx.EVT_BUTTONButton click
wx.EVT_TEXTText input changes
wx.EVT_CHECKBOXCheckbox toggle
wx.EVT_SLIDERSlider dragged
wx.EVT_CLOSEWindow close button
wx.EVT_MENUMenu item selected
wx.EVT_KEY_DOWNKey pressed

LESSON 08

Menus & Dialogs

80% complete

Native menus and standard dialogs are wxPython superpowers — they integrate perfectly with the macOS menu bar and dialog system.

Python — menu bar + file dialog
import wx

class MenuFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Menus Demo', size=(450, 300))

        # Build the menu bar
        menubar = wx.MenuBar()
        file_menu = wx.Menu()
        open_item = file_menu.Append(wx.ID_OPEN, '&Open...\tCtrl+O')
        file_menu.AppendSeparator()
        quit_item = file_menu.Append(wx.ID_EXIT, '&Quit\tCmd+Q')
        menubar.Append(file_menu, '&File')
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.on_open, open_item)
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), quit_item)

        panel = wx.Panel(self)
        self.info = wx.StaticText(panel, label='Use File → Open')
        self.Centre(); self.Show()

    def on_open(self, event):
        dlg = wx.FileDialog(self, 'Open file',
            wildcard='Text files (*.txt)|*.txt|All files (*.*)|*.*',
            style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
        if dlg.ShowModal() == wx.ID_OK:
            self.info.SetLabel(f'Opened: {dlg.GetPath()}')
        dlg.Destroy()

wx.App(); MenuFrame(); wx.GetApp().MainLoop()

Common Built-in Dialogs

DialogPurpose
wx.FileDialogOpen/save file picker
wx.DirDialogFolder chooser
wx.MessageDialogAlert/confirm/info box
wx.TextEntryDialogSingle line text input dialog
wx.ColourDialogSystem colour picker
wx.FontDialogSystem font picker

LESSON 09

Custom Drawing with wx.PaintDC

90% complete

For custom 2D graphics, draw to a wx.Panel using a Device Context (DC) inside an EVT_PAINT handler.

Python — custom 2D drawing
import wx

class DrawPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        self.Bind(wx.EVT_PAINT, self.on_paint)

    def on_paint(self, event):
        dc = wx.PaintDC(self)
        dc.SetBackground(wx.Brush('#1a1a2e'))
        dc.Clear()

        # Draw a gradient-like bar chart
        colours = ['#ff9f43', '#ee5a24', '#0097e6', '#00a8ff']
        values  = [80, 55, 120, 95]
        for i, (h, c) in enumerate(zip(values, colours)):
            dc.SetBrush(wx.Brush(c))
            dc.SetPen(wx.TRANSPARENT_PEN)
            dc.DrawRectangle(60 + i * 90, 200 - h, 60, h)

        # Labels
        dc.SetTextForeground('white')
        dc.DrawText('Bar Chart Demo', 140, 20)

class DrawFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Custom Drawing', size=(420, 260))
        DrawPanel(self)
        self.Centre(); self.Show()

wx.App(); DrawFrame(); wx.GetApp().MainLoop()
ℹ️ Always use PaintDC inside EVT_PAINT

For drawing outside a paint event (e.g., on a timer), use wx.ClientDC. For double-buffered flicker-free animation use wx.BufferedPaintDC.


LESSON 10

Capstone — A Note-Taking App

100% complete 🎉

Let's combine everything — menus, sizers, TextCtrl, and events — into a functional native macOS note-taking app with save capability.

Python — notes_app.py
import wx

class NotesApp(wx.Frame):
    def __init__(self):
        super().__init__(None, title='Notes', size=(580, 460))
        self.current_file = None

        # ── Menus ──
        mb = wx.MenuBar()
        fm = wx.Menu()
        fm.Append(wx.ID_NEW,  'New\tCmd+N')
        fm.Append(wx.ID_OPEN, 'Open...\tCmd+O')
        fm.Append(wx.ID_SAVE, 'Save\tCmd+S')
        fm.AppendSeparator()
        fm.Append(wx.ID_EXIT, 'Quit\tCmd+Q')
        mb.Append(fm, 'File')
        self.SetMenuBar(mb)

        self.Bind(wx.EVT_MENU, self.on_new,  id=wx.ID_NEW)
        self.Bind(wx.EVT_MENU, self.on_open, id=wx.ID_OPEN)
        self.Bind(wx.EVT_MENU, self.on_save, id=wx.ID_SAVE)
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), id=wx.ID_EXIT)

        # ── Editor ──
        panel = wx.Panel(self)
        self.editor = wx.TextCtrl(
            panel, style=wx.TE_MULTILINE | wx.TE_RICH2,
            font=wx.Font(14, wx.FONTFAMILY_MODERN,
                          wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
        )
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.editor, 1, wx.EXPAND | wx.ALL, 8)
        panel.SetSizer(sizer)

        self.CreateStatusBar()
        self.SetStatusText('New document')
        self.Centre(); self.Show()

    def on_new(self, _):
        self.editor.SetValue('')
        self.current_file = None
        self.SetStatusText('New document')

    def on_open(self, _):
        dlg = wx.FileDialog(self, wildcard='*.txt', style=wx.FD_OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            self.current_file = dlg.GetPath()
            with open(self.current_file) as f:
                self.editor.SetValue(f.read())
            self.SetStatusText(self.current_file)

    def on_save(self, _):
        if not self.current_file:
            dlg = wx.FileDialog(self, wildcard='*.txt',
                                  style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT)
            if dlg.ShowModal() != wx.ID_OK: return
            self.current_file = dlg.GetPath()
        with open(self.current_file, 'w') as f:
            f.write(self.editor.GetValue())
        self.SetStatusText(f'Saved: {self.current_file}')

wx.App(); NotesApp(); wx.GetApp().MainLoop()
🎓 What You Learned

You've installed wxPython, used Frames, Panels, Controls, Sizers, native Menus, File Dialogs, Events, and built a complete native macOS text editor. Next: explore wxpython.org, wx.aui for docking panels, wx.grid.Grid for spreadsheet-style tables, and pyinstaller for packaging your app as a .app bundle.