Jump to: pin numbering, an LED, a button, PWM, running as a service or troubleshooting.
Before you start
The 40-pin header on a Raspberry Pi connects your code to the physical world: LEDs, buttons, relays, motors and sensors. This guide drives those pins from Python with gpiozero, the library Raspberry Pi OS ships and recommends. It works the same way on every model, including the Pi 5, where the GPIO hardware changed and several older libraries stopped working.
You'll need:
- A Raspberry Pi with the 40-pin header (any model from the Pi 2 on, the Zero 2 W, or a Pi 5) running a current Raspberry Pi OS. The Raspberry Pi Setup Guide covers the install.
- A breadboard and jumper wires (female-to-male for connecting to the header).
- An LED and a 330 Ω resistor, and a push button.
- Optionally, a relay module rated for 3.3 V logic.
Two rules protect the Pi, because the GPIO pins connect directly to its processor with no protection:
- GPIO pins work at 3.3 V. Connecting 5 V to a GPIO pin, from a 5 V sensor or the 5 V pin on the header, can permanently damage the processor. Use a level shifter for 5 V devices.
- Each pin can only supply a few milliamps. Keep it to about 16 mA per pin, and much less in total across all pins. That's enough for an LED through a resistor, but not for a motor, a relay coil or a strip of LEDs. Drive those through a transistor, a driver board or a relay module.
Wire things up with the Pi shut down and unplugged, and double-check before you power it on.
Pin numbering
Each header pin has two numbers, and confusing them is the most common GPIO mistake:
- Physical pin numbers count the header positions, 1 to 40. Pin 1 is the square pad, nearest the corner of the board.
- GPIO (BCM) numbers are the processor's own names, such as GPIO17. They don't follow the physical order.
gpiozero uses GPIO numbers by default, so LED(17) means GPIO17, which is physical pin 11. To print a map of your Pi's header in the terminal:
pinout
The pinout command comes with gpiozero. The pins used in this guide:
| Use | GPIO | Physical pin |
|---|---|---|
| LED | GPIO17 | 11 |
| Button | GPIO27 | 13 |
| PWM LED | GPIO18 | 12 |
| Relay | GPIO22 | 15 |
| Ground | GND | 6, 9, 14, 20, 25, 30, 34 or 39 |
| 3.3 V supply | 3V3 | 1 or 17 |
Some pins have special jobs: GPIO2 and 3 are the I²C bus and have built-in pull-up resistors, GPIO14 and 15 are the serial console, and GPIO0 and 1 are reserved for HAT identification. Leave those alone for simple projects.
Software setup
Versions: the examples use gpiozero's long-standing classes (LED, Button, PWMLED, OutputDevice, MotionSensor), which exist in both the 1.6 and 2.x series. Check what you have with python3 -c "import gpiozero; print(gpiozero.__version__)" and compare against the gpiozero documentation if a class or argument is rejected.
gpiozero is installed on Raspberry Pi OS with a desktop. On Raspberry Pi OS Lite, install it from the repository:
sudo apt update
sudo apt install python3-gpiozero python3-lgpio
python3 -c "import gpiozero; print(gpiozero.__version__)"
gpiozero is a friendly layer over a lower-level pin factory that actually drives the pins. On current releases that's lgpio, which works on every model, including the Pi 5. Your user needs to be in the gpio group, which the default account already is. Check with groups.
The library that older tutorials use, RPi.GPIO, doesn't work on the Pi 5: its GPIO pins are on a separate chip, the RP1, that RPi.GPIO doesn't know about. Code written for it fails there with RuntimeError: Cannot determine SOC peripheral base address. gpiozero avoids the problem, so it's the better choice for new code on any model.
If you use a virtual environment, create it with access to the system packages so it can see gpiozero and lgpio: python3 -m venv --system-site-packages .venv.
An LED
Connect the LED's longer leg (the anode) to GPIO17 through the 330 Ω resistor, and the shorter leg (the cathode, by the flat side of the rim) to a ground pin. The resistor limits the current to around 4 mA, well within the pin's limit.
from gpiozero import LED
from signal import pause
led = LED(17)
led.blink(on_time=0.5, off_time=0.5)
pause()
python3 blink.py # Ctrl+C to stop
blink() runs in a background thread, so the script needs pause() to keep running. When it exits, gpiozero sets the pin back to an input, which turns the LED off. For direct control, use led.on(), led.off() and led.toggle().
If the LED doesn't light, turn it around. LEDs only conduct one way, and a reversed one is harmless.
A button
Connect one side of the button to GPIO27 and the other to a ground pin. No resistor is needed: gpiozero turns on the pin's internal pull-up resistor, so the pin reads high until the button connects it to ground.
from gpiozero import LED, Button
from signal import pause
led = LED(17)
button = Button(27, bounce_time=0.05)
button.when_pressed = led.toggle
button.when_held = lambda: print("held for a second")
button.hold_time = 1.0
pause()
Each press now toggles the LED. A few details are worth understanding:
- Bounce. When the contacts of a mechanical switch close, they bounce for a few milliseconds and produce several presses.
bounce_time=0.05ignores further changes for 50 ms after the first one. - Callbacks such as
when_pressedrun in a background thread whenever the event happens. Assign a function, without brackets:led.toggle, notled.toggle(), which would call it once immediately and assign the result. - Long-running callbacks delay the next event. If a press has to do something slow, such as a network request, hand the work to another thread or a queue.
To wait for a press in a straight-line script instead, use button.wait_for_press().
Connecting devices together
gpiozero devices have a value and a source. Setting one device's source to another links them, so the first follows the second without any loop:
from gpiozero import LED, Button
from signal import pause
led = LED(17)
button = Button(27)
led.source = button # the LED is lit exactly while the button is held
pause()
This style is short and doesn't busy-wait, but it hides what's happening. For anything beyond a direct link, callbacks are easier to follow.
Dimming with PWM
A GPIO pin is either on or off. Pulse-width modulation switches it on and off quickly and varies the share of time it's on, the duty cycle. An LED driven at a 25% duty cycle looks dim, because your eye averages the flicker.
from gpiozero import PWMLED
from signal import pause
led = PWMLED(18)
led.pulse(fade_in_time=1.5, fade_out_time=1.5)
pause()
Move the LED and resistor to GPIO18 for this one. led.value = 0.25 sets a fixed brightness between 0 and 1.
gpiozero's default PWM is generated in software, which is fine for LEDs but has some timing jitter. Servos need accurate pulses, and jitter shows up as twitching. For servos, use the hardware PWM pins, GPIO12, 13, 18 and 19, and drive servos from their own power supply, never from the Pi's 5 V pin, since a servo starting up can pull enough current to reset the Pi.
Switching a relay
A relay lets a GPIO pin switch a separate circuit, such as a fan or a pump. Use a relay module, a small board with a relay, a transistor to drive its coil and a protection diode. A bare relay coil connected straight to a GPIO pin draws more current than the pin can supply, and its switching spike can damage the Pi.
Many modules are "active low": the relay turns on when the input is pulled to ground. gpiozero handles that with active_high=False:
from gpiozero import OutputDevice
from time import sleep
relay = OutputDevice(22, active_high=False, initial_value=False)
relay.on()
sleep(5)
relay.off()
initial_value=False makes sure the relay starts off when the script starts. Check the module's input is 3.3 V compatible. Some modules sold for Arduino expect 5 V logic and switch unreliably, or not at all, from a Pi.
Mains voltage is a different matter. Switching 230 V or 120 V devices needs proper enclosures, wiring and fusing, and in many places a qualified electrician. Use a pre-built, certified smart plug or relay unit for mains loads.
Reading a digital sensor
Many simple sensors have a single digital output: a PIR motion sensor, a reed switch or a water-level float. gpiozero has classes for common ones:
from gpiozero import MotionSensor, LED
from signal import pause
pir = MotionSensor(4)
light = LED(17)
pir.when_motion = light.on
pir.when_no_motion = light.off
pause()
HC-SR501 PIR modules run from 5 V, but their output is 3.3 V, so they're safe to connect to a GPIO pin. Check the output level of any sensor powered from 5 V before connecting it. For sensors that use I²C, such as the BME280 used elsewhere on this site, enable I²C with sudo raspi-config under Interface Options and read them with a library for that sensor. The ESP32 BME280 Weather Station shows the wiring.
Running a script at boot
A script started from the terminal stops when you log out. To run one permanently, make it a systemd service. Save your script as /home/pi/gpio/button_led.py, using your own user and path, and create a unit file:
[Unit]
Description=Button-controlled LED
After=multi-user.target
[Service]
Type=simple
User=pi
Group=gpio
WorkingDirectory=/home/pi/gpio
ExecStart=/usr/bin/python3 /home/pi/gpio/button_led.py
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now button-led.service
systemctl status button-led.service --no-pager
journalctl -u button-led.service -f
Run it as your normal user, not root. Membership of the gpio group is all it needs. PYTHONUNBUFFERED=1 makes print() output appear in the journal straight away. The systemd services guide explains the unit file options in detail.
Cleaning up
gpiozero closes its devices and resets their pins when the script exits normally, including after Ctrl+C. To make shutdown explicit, for example when systemd stops the service with SIGTERM, handle the signal and close devices yourself:
from gpiozero import LED
from signal import signal, SIGTERM, pause
import sys
led = LED(17)
def shutdown(signum, frame):
led.close()
sys.exit(0)
signal(SIGTERM, shutdown)
led.on()
pause()
A closed device releases its pin. Without this, a relay that was on when the service was killed could stay on until the pin is next used.
Troubleshooting
RuntimeError: Cannot determine SOC peripheral base address
The code uses RPi.GPIO on a Pi 5. Switch to gpiozero. For code you can't change, the rpi-lgpio package provides a compatible replacement for most of the RPi.GPIO interface.
GPIO busy or lgpio.error: 'GPIO busy'
Another program, or another copy of your script, already uses that pin. Check for a running service with systemctl list-units --type=service or ps aux | grep python. Pins claimed by a device-tree overlay in /boot/firmware/config.txt, for example by a one-wire or I²C overlay, are busy too.
The button triggers on its own
The input is floating. Make sure the button connects the pin to ground, which is what gpiozero's default pull-up expects. Long wires pick up interference; keep them short, or add an external 10 kΩ pull-up and a 100 nF capacitor to ground at the Pi end.
Permission denied when opening the GPIO chip
Your user isn't in the gpio group. Add it with sudo usermod -aG gpio $USER, then log out and back in.
Nothing happens and there's no error
Check you're using GPIO numbers, not physical pin numbers. LED(11) is GPIO11, physical pin 23, not physical pin 11. Run pinout and count again.
Next steps
- Mosquitto MQTT Broker Setup: publish button presses and sensor readings over MQTT with the
paho-mqttlibrary. - Home Assistant in Docker: control the relay from a dashboard.
- Raspberry Pi Setup Guide: install and secure the Pi itself.
- The gpiozero recipes and Raspberry Pi's GPIO documentation.