IoT 📖 11 min read

Raspberry Pi GPIO with Python

The 40-pin header connects a Raspberry Pi to LEDs, buttons, relays and sensors. This guide drives it from Python with gpiozero, the library Raspberry Pi OS recommends, which works the same on every model, including the Pi 5, where older libraries stopped working. It starts with the two electrical rules that keep the Pi safe and ends with a script that runs at boot as a proper service.

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:

UseGPIOPhysical pin
LEDGPIO1711
ButtonGPIO2713
PWM LEDGPIO1812
RelayGPIO2215
GroundGND6, 9, 14, 20, 25, 30, 34 or 39
3.3 V supply3V31 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.

blink.py
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.

button_led.py
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.05 ignores further changes for 50 ms after the first one.
  • Callbacks such as when_pressed run in a background thread whenever the event happens. Assign a function, without brackets: led.toggle, not led.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.

breathe.py
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:

/etc/systemd/system/button-led.service
[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