RPi.GPIO has been the default choice for Python GPIO code on a Raspberry Pi for over a decade, but its release cadence has slowed a lot, and it talks to the GPIO hardware through direct /dev/mem register access -- an approach that doesn't match the newer GPIO character-device model the kernel has moved to, and that outright breaks on the Pi 5's BCM2712 SoC without a compatibility shim. If you've got an old script you still rely on, it's worth porting it rather than patching around the problem forever.
Why this matters now
On a Pi 5, plain pip install RPi.GPIO either fails to build or installs but can't access the GPIO pins, because BCM2712 moved pin control behind the kernel's gpiochip character device instead of exposing raw memory-mapped registers the way BCM2711 and earlier did. The community shim rpi-lgpio (sudo apt install python3-rpi-lgpio) monkey-patches the RPi.GPIO API onto the modern lgpio backend and will get an old script running again on a Pi 5 with zero code changes -- useful as a stopgap, but it's a translation layer on top of a translation layer. The two real destinations are:
- gpiozero -- a higher-level library with device classes (
LED,Button,PWMLED,DistanceSensor...) built on swappable pin factories. It already defaults tolgpioon a Pi 5 and handles debounce, PWM, and cleanup for you. This is the right target for most application code. - libgpiod v2 (the
gpiodPython package) -- talks straight to the kernel'sgpiochipcharacter device. No Pi-specific assumptions, works the same way on any Linux board that exposes a gpiochip, and has the least overhead. Reach for it when you need portability across boards, or when gpiozero's abstractions get in the way of something low-level.
API differences at a glance
| Operation | RPi.GPIO | gpiozero | libgpiod v2 (gpiod) |
|---|---|---|---|
| Setup | GPIO.setmode(GPIO.BCM)GPIO.setup(pin, GPIO.OUT) | led = LED(pin) | gpiod.request_lines(chip, config={...}) |
| Read input | GPIO.input(pin) | button.is_pressed | request.get_value(pin) |
| Write output | GPIO.output(pin, GPIO.HIGH) | led.on() / led.off() | request.set_value(pin, Value.ACTIVE) |
| Pull resistor | pull_up_down=GPIO.PUD_UP | Button(pin, pull_up=True) | bias=Bias.PULL_UP in LineSettings |
| Edge callback | GPIO.add_event_detect(...) | button.when_pressed = fn | request.read_edge_events() |
| Debounce | bouncetime=ms | bounce_time=seconds | manual, via edge event timestamps |
| Cleanup | GPIO.cleanup() | automatic (garbage collection / context) | with ... as request: context manager |
Worked example: an LED that follows a button
This is the kind of script that's all over old Raspberry Pi tutorials. Before, with RPi.GPIO:
import RPi.GPIO as GPIO
import time
LED = 27
BUTTON = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED, GPIO.OUT)
GPIO.setup(BUTTON, GPIO.IN, pull_up_down=GPIO.PUD_UP)
try:
while True:
if GPIO.input(BUTTON) == GPIO.LOW:
GPIO.output(LED, GPIO.HIGH)
else:
GPIO.output(LED, GPIO.LOW)
time.sleep(0.01)
except KeyboardInterrupt:
GPIO.cleanup()
After, with gpiozero -- no polling loop, no manual cleanup, and it reads like what it's doing:
from gpiozero import LED, Button
from signal import pause
led = LED(27)
button = Button(17)
button.when_pressed = led.on
button.when_released = led.off
pause()
Install with pip install gpiozero lgpio. gpiozero picks the lgpio pin factory automatically on a Pi 5; on older Pis it'll still work, but you can force a specific backend with the GPIOZERO_PIN_FACTORY environment variable if you ever need to (e.g. GPIOZERO_PIN_FACTORY=pigpio for remote-GPIO use cases).
The same thing again with libgpiod v2, for when you want to drop the Pi-specific abstraction entirely:
import gpiod
from gpiod.line import Direction, Value, Bias
LED = 27
BUTTON = 17
with gpiod.request_lines(
"/dev/gpiochip0",
consumer="led-button",
config={
LED: gpiod.LineSettings(direction=Direction.OUTPUT),
BUTTON: gpiod.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP),
},
) as request:
try:
while True:
pressed = request.get_value(BUTTON) == Value.INACTIVE
request.set_value(LED, Value.ACTIVE if pressed else Value.INACTIVE)
except KeyboardInterrupt:
pass
Install with sudo apt install python3-libgpiod or pip install gpiod (make sure you get version 2.x -- the 1.x API is a different interface). Note the pin numbers here refer to offsets on the gpiochip, not necessarily the same numbering as BCM pin numbers on every board; on a Pi they line up, but check with gpioinfo if you're porting to other hardware.
Which one to pick
For almost all application and hobby code -- anything that reads sensors, drives LEDs or motors, debounces buttons, generates PWM -- gpiozero is the better target. It's actively maintained, has sane defaults, and the resulting code is shorter and more readable than either alternative. Reach for libgpiod v2 directly when you're writing something that needs to run unmodified on non-Pi single-board computers, when you want the smallest possible dependency footprint on a minimal image, or when you're doing something gpiozero doesn't model well, like reading a burst of edge timestamps for a rotary encoder at high resolution.
Either way, the rpi-lgpio shim is a reasonable bridge if you need an old script working again today, but treat it as temporary -- it's built against the RPi.GPIO API precisely so you can migrate later without a hard deadline, not so you never have to.
