Python & GPIO: code meets circuit
420-302-VA · WEEK 4 · FALL 2026

Stage 5 of 6 · Lab, kit and Pi · about 40 minutes

First light

Everything converges here: the terminal from Week 2, the SSH key from Week 3, the environment from an hour ago, and the circuit rules from the last page. Fifteen lines of Python, one LED, one button, and the semester's first complete system.

Wire the circuit

Wiring diagram: from the Pi header, GPIO17 at physical pin 11 goes to a 330 ohm resistor then the LED anode on the breadboard, LED cathode to ground at physical pin 6. The button sits across the breadboard gap, one side to GPIO27 at physical pin 13, the other side to ground at physical pin 14. Pi header (top rows, odd pins inside) 1 · 3V3 6 · GND 11 · GPIO17 13 · GPIO27 14 · GND Breadboard centre gap 330 Ω LED · long leg left button · across the gap copper wires: signals from GPIO pins · black wires: returns to ground
Two independent loops. LED loop: GPIO17 → 330 Ω → LED → GND. Button loop: GPIO27 → button → GND (the pull-up is inside the Pi). Female ends on the header, male ends in the breadboard.
  1. Power down or stop outputs

    Wire before running anything: either the Pi is off, or no GPIO script is running. Rule 5 from the previous page.

  2. LED loop

    Jumper from physical 11 (GPIO17) to a breadboard row. Resistor from that row to a second row. LED from the second row (long leg here) across to a third row. Jumper from the third row back to physical 6 (GND).

  3. Button loop

    Seat the button across the centre gap so its legs cannot short each other. Jumper from physical 13 (GPIO27) to one leg's row; jumper from the diagonally opposite leg's row to physical 14 (GND).

  4. Trace it aloud

    Pin by pin against the diagram, both of you: operator points, verifier reads. Count the physical pins from the corner with the square pad; do not trust memory.

Over SSH, in the activated environment ((.venv) in the prompt), create blink.py with nano:

from gpiozero import LED
from time import sleep

led = LED(17)          # BCM number: GPIO17, the wire on physical pin 11

for n in range(10):
    led.on()
    sleep(0.5)
    led.off()
    sleep(0.5)
    print(f"blink {n}")
(.venv) $ python blink.py

Ten blinks, then the script ends and the pin releases. If nothing lights, resist the urge to rewire at random: the troubleshoot table has the LED cases in order (backwards LED first, wrong pin second). Then experiment, one variable at a time: pull the delay into delay = 0.2 and feel the code-to-circuit loop close.

What gpiozero just did for you

gpiozero is the Raspberry Pi Foundation's own library, and the three lines hide honest work worth knowing about:

  • LED(17) claimed GPIO17 from the OS (through a low-level pin driver), configured it as an output, and registered a cleanup so the pin is released and switched off when the script exits, which is why a crashed script does not leave the LED stuck on.
  • .on() / .off() drive the pin high and low. There is also .toggle(), and .blink() which blinks in the background without your loop.
  • The same pattern extends to every component this semester: Button, Buzzer, MotionSensor, DistanceSensor and dozens more, each documented with a wiring diagram in the gpiozero recipes. Learning one class teaches the shape of all of them.

Read the button

New file, press.py, the polling version, a scan cycle you write yourself:

from gpiozero import Button
from time import sleep

button = Button(27)    # internal pull-up enabled; button wired to GND

while True:
    if button.is_pressed:
        print("pressed")
    else:
        print("released")
    sleep(0.2)

Run it, hold and release the button, watch the truth arrive five times a second, stop with Ctrl+C. This is exactly how a PLC sees an input, and its cost is visible too: the loop burns CPU asking a question whose answer rarely changes, and a press shorter than 0.2 s can slip between polls.

Events: when_pressed

The alternative: tell the library what to do and let it watch the pin. events.py:

from gpiozero import Button
from signal import pause

def hello():
    print("pressed!")

button = Button(27, bounce_time=0.05)
button.when_pressed = hello      # the function itself, no parentheses

pause()                          # sleep forever; events do the work
  • when_pressed = hello hands gpiozero a function to call on each press. Note the absence of (): you are giving it the recipe, not the result of running it once.
  • bounce_time=0.05 is the software debounce from the inputs section: contact chatter within 50 ms counts as one press.
  • pause() keeps the script alive doing nothing, while presses fire your function. No polling loop, no missed short presses, no wasted CPU. Event-driven code is how the MQTT programs of Week 9 will be shaped, so this small script is a preview of the course's second half.

Put them together

The week's finale, toggle.py: the button drives the LED, and the wiring does not change at all.

from gpiozero import LED, Button
from signal import pause

led = LED(17)
button = Button(27, bounce_time=0.05)

button.when_pressed = led.toggle   # each press flips the LED

pause()

Five meaningful lines: a light switch, implemented in software. Change the behaviour without touching a wire, led.toggle to led.on, add button.when_released = led.off for a momentary switch, and you have demonstrated the entire point of putting a computer between the input and the output. Commit all four scripts and push; the hand-in collects them.

Why this is the whole course in miniature

Sense (button) → decide (your Python) → act (LED). The term project is this triangle with better senses (temperature, light, soil moisture), richer decisions (thresholds, PID), stronger actions (relays, pumps) and a dashboard watching, but the triangle never changes.

Checklist for this stage

Check yourself

Why LED(17) and not LED(11) for a wire on pin 11?
gpiozero uses BCM numbering: the SoC channel GPIO17 happens to sit at physical position 11. The translation is deliberate and per-pin; pinout or pinout.xyz settles any doubt.
Polling versus events: name one concrete weakness of the polling loop that when_pressed fixes.
Either answer: a press shorter than the polling interval can be missed entirely, or the loop consumes CPU continuously to ask an almost-always-unchanged question. Events invert control: the library watches, your function runs only when something happens.
In button.when_pressed = hello, why no parentheses on hello?
With parentheses you would call the function once, now, and assign its result. Without them you hand over the function itself, for gpiozero to call at every future press. Functions are values in Python; this line is the first place that fact earns money.
What is bounce_time=0.05 compensating for, physically?
Mechanical contact bounce: the button's metal parts chatter open and closed for a few milliseconds on each press. Debouncing merges that chatter into one logical event, in software here, sometimes with an RC filter in hardware.
What does pause() do, and what would happen without it?
It blocks forever, keeping the script alive so events can fire. Without it the script reaches its end and exits immediately, releasing the pins; the toggle would never see a press.