Objects & classes: state and behaviour move in together
420-302-VA · WEEK 6 · FALL 2026

Stage 4 of 6 · Theory, kit optional · about 25 minutes

State & devices

Week 5 ended with state machines held together by discipline: a mode variable here, a flag there, functions everywhere trusted to respect them. A class holds them together by construction. This page turns a Week 5 state diagram into a device class, the pattern behind every system you build from now to December.

Giving state a home

Recall the Week 5 two-mode night light: NORMAL and NIGHT, a press toggles, presses ignored for 1 s after a switch. Its loose-variable implementation needs a mode variable, a last_switch timestamp, the hardware objects, and a function that touches all of them, four things whose only connection is your memory. The class version makes the connection physical: they become attributes of one object, and the transition rule becomes its method. State machines and classes are not merely compatible; the class is the state machine's natural container, states as attribute values, transitions as methods.

Worked example: NightLight

from gpiozero import LED, Button
from signal import pause
from time import monotonic

class NightLight:
    def __init__(self, led_pin, button_pin):
        self.led = LED(led_pin)                      # has-a
        self.button = Button(button_pin, bounce_time=0.05)
        self.mode = "NORMAL"                         # the state, at home
        self.last_switch = 0.0
        self.button.when_pressed = self.press        # my method as the callback

    def press(self):
        if monotonic() - self.last_switch < 1.0:     # the 1 s ignore rule
            return
        if self.mode == "NORMAL":
            self.mode = "NIGHT"
            self.led.on()
        else:
            self.mode = "NORMAL"
            self.led.off()
        self.last_switch = monotonic()

light = NightLight(17, 27)
print("Night light running. Ctrl+C to stop.")
pause()

Read it top to bottom against what you already know: the constructor takes pin numbers (the facts a NightLight cannot invent), builds its own gpiozero objects, sets its starting state, and wires its own callback. press() is the entire behaviour of the system, in one place, guarding its own state. The main program shrinks to two meaningful lines, which is the sign of a well-drawn capsule boundary.

State diagram to class, mapped

On the Week 5 diagramIn the class
A state (NORMAL, NIGHT)A value of self.mode
The current state markerWhat self.mode holds right now
An event (button pressed)A method being called (press())
A transition's guard condition (≥ 1 s since switch)The if at the top of the method
A transition's action (LED on/off)The code in the branch that changes self.mode
Extra memory the machine carriesMore attributes (self.last_switch)

This table is a two-way translator. Given a state diagram (Week 5's design lab produced several), you can now write its class mechanically; given a class like this, you can draw its diagram, and drawing it is the fastest way to review someone else's state logic. GRAFCET-trained eyes will recognize the whole arrangement: steps, transitions, guards, actions, in Python clothes.

Methods as callbacks

The line self.button.when_pressed = self.press is Week 4's event pattern upgraded. self.press, no parentheses, is the method as a thing (the bound-method fact from two pages ago), and handing it to the Button means: when the press happens, call this method on this instance. The object wires its own reflexes in its own constructor. Two details with teeth:

  • No parentheses. when_pressed = self.press() would run the method once, now, and assign its return value (None) as the callback. The system then ignores every press, with no error anywhere. Week 4's rule, now with self.
  • The pattern is the course's future. Week 9's MQTT client works exactly like this: you build a client object and assign your methods to its on_connect and on_message hooks. Learn the shape once here, spend it twice later.

Composition: has-a

The NightLight object drawn as an outer capsule containing an LED object, a Button object, and its own state: mode NORMAL and last_switch. An arrow labelled when_pressed runs from the Button to the press method on the capsule wall. NightLight instance (light) LED self.ledpin 17 Button self.buttonpin 27 own state mode: "NORMAL"last_switch press() when_pressed
Objects inside objects. The NightLight has an LED, has a Button, has a mode; the parts stay whole and tested, and the container adds only the logic that relates them. Composition is how every project system this term will be built: your classes wrapping gpiozero (and later MQTT) objects, not replacing them.

Design guidance that will serve you through the project: when tempted to write a big class, ask what it has. A greenhouse controller has a light sensor, has a pump relay, has a mode; each part is its own object with its own class, and the controller composes them. Small capsules, clearly related, beat one giant one, and the requirements document (Week 5) usually tells you where the seams are: nouns become classes, behaviours become their methods.

The payoff: encapsulation

Notice what is now impossible. No line outside NightLight can flip the light without going through press(), so the 1 s rule cannot be forgotten by a future caller; the mode and the LED cannot drift out of sync, because the only code that changes one changes both. State changed exclusively through the methods that know its rules is encapsulation, and it is the same safety philosophy as a guarded panel: not "trust everyone to be careful" but "build it so carelessness has no path". Python enforces this by convention rather than by lock (any code could write light.mode = "NIGHT"), so the discipline is a team norm: touch state through methods, and treat a teammate's attributes as read-only from outside. The OOP article covers the concept alongside its siblings.

Inheritance, for reading

Open gpiozero's LED documentation and the first line under the class name reads "Bases: DigitalOutputDevice". That is inheritance: LED is written as a specialization of DigitalOutputDevice (an LED is a digital output device), and receives everything the parent defines, on(), off(), blink(), value, for free, adding or adjusting only what makes an LED specific. Reading rules, which are all this course requires of you:

  • A class's full ability = its own listing plus its parents', up the chain. A method you use but cannot find is defined on a base; follow the "Bases:" links.
  • help(LED) in the REPL shows the inherited members merged, "Methods inherited from ..." headers included; often faster than the chain-walk.
  • Is-a vs has-a: an LED is a digital output device (inheritance); your NightLight has an LED (composition). This course writes has-a and reads is-a; writing your own subclasses is real and useful, and belongs to a later course, not to your first month of Python.

Checklist for this stage

Check yourself

Map it: on the state diagram, what do a state, an event and a guard become in the class?
A state is a value of self.mode; an event is a method call; a guard is the if that decides whether the transition happens. Actions are the branch's code; extra machine memory becomes more attributes.
Why does the 1 s ignore rule live inside press() rather than in the main program?
Encapsulation: the rule protects the mode, so it belongs to the only code allowed to change the mode. Every caller, present and future, gets the rule enforced without remembering it exists.
What does self.button.when_pressed = self.press hand to the Button, exactly?
The bound method: press tied to this specific instance. When the hardware event fires, gpiozero calls it, which runs press() with self already set to this NightLight.
Your teammate's version toggles the LED but sometimes shows mode NORMAL with the light on. Diagnose in OOP terms.
Some code path changes the LED without changing self.mode (or vice versa), meaning state is being touched outside the method that owns the rules. The fix is structural: route every change through the transition method so the two can never diverge.
An LED "is a" DigitalOutputDevice; your NightLight "has an" LED. Why does the difference matter when reading docs?
Is-a means LED inherits the parent's methods, so its full ability is spread across the "Bases:" chain and you must read upward. Has-a means NightLight's parts are attributes with their own docs; nothing is inherited, you just follow the dots: light.led.blink.
Where does this exact callback pattern reappear later in the course?
Week 9: paho-mqtt's client object, whose on_connect and on_message hooks you assign your own methods to, and Week 11's Flask routes follow the same spirit. One pattern, three technologies.