Python Programming with Raspberry Pi: GPIO, Sensors, asyncio, and Real Projects

The Raspberry Pi is a credit-card-sized computer that runs a full Linux operating system, and Python is its native language. Together, they give you a platform where code reaches out and touches the real world — blinking lights, reading sensors, driving motors, and connecting to the internet. That sentence seems to describe a toy. What it actually describes is the foundation of thousands of real production systems: environmental monitors, industrial controllers, wildlife camera traps, satellite ground stations, and school networks. The gap between "blink an LED" and "run a real embedded system" is smaller on a Raspberry Pi than anywhere else in computing.

Python has been the go-to language for Raspberry Pi since the board's earliest days, and that relationship has only deepened. The name "Pi" in Raspberry Pi was partly chosen as a nod to Python itself. Whether you are working on a Raspberry Pi 5 with its powerful BCM2712 chip and in-house RP1 southbridge, or an older Pi 4 sitting in a drawer, Python remains the most supported, most documented, and frankly the most enjoyable way to program it. This article is part of PythonCodeCrack's library of python tutorials and walks you through everything from initial setup through controlling hardware and building real projects.

What Makes Raspberry Pi Different

When you write Python on your laptop, the code lives entirely in software. On a Raspberry Pi, your Python scripts can talk directly to the physical world through the board's 40-pin GPIO header — GPIO standing for General Purpose Input/Output. Those pins can be configured as inputs, reading voltage from a button or sensor, or as outputs, sending voltage to an LED, relay, or motor driver. This is what separates a Raspberry Pi from a regular computer.

The Raspberry Pi 5, the current flagship released in late 2023, remains the dominant single-board model in 2026 (the Raspberry Pi 500 desktop computer, launched in late 2024, uses the same BCM2712 chip in a keyboard form factor). It is significantly faster than its predecessors. Its RP1 chip handles all I/O duties, which introduced an important change: the old RPi.GPIO library no longer works on the Pi 5 due to a changed memory mapping. If you are moving code from a Pi 4 to a Pi 5, this is the first thing to know. The recommended replacement is gpiozero, which is pre-installed in Raspberry Pi OS and explicitly supported by Raspberry Pi Ltd for all current hardware.

Pi 5 GPIO Compatibility

On the Raspberry Pi 5, RPi.GPIO and pigpio do not work. Use gpiozero (which uses lgpio as its backend) for new projects. For existing code written against RPi.GPIO, the rpi-lgpio drop-in replacement on PyPI restores compatibility without requiring code changes.

Unlike a microcontroller such as Arduino, the Raspberry Pi runs a full operating system — Raspberry Pi OS, a Debian-based Linux distribution. That means you have pip, virtual environments, the entire Python ecosystem, SSH, a web browser, and a desktop environment all available at once. The trade-off is that the Pi is not designed for hard real-time control. For tasks requiring microsecond-level timing precision, a microcontroller is a better fit. For everything else — web servers, data logging, computer vision, home automation, machine learning inference — the Pi paired with Python is a formidable combination.

Getting Python Running on Your Pi

Raspberry Pi OS ships with Python 3 pre-installed, and the version you get depends entirely on which OS release you are running. As of early 2026, there are two current releases to be aware of: Raspberry Pi OS Bookworm (based on Debian 12, released October 2023) ships Python 3.11 and remains supported for at least two more years. Raspberry Pi OS Trixie (based on Debian 13, released October 2025) ships Python 3.13. If you flashed a new SD card recently and ran all updates, you are likely on Trixie. If your Pi has been running for a year or more without a reinstall, you are probably on Bookworm. You can check both your OS and Python version from a terminal:

python3 --version
lsb_release -a

This distinction matters because some packages — particularly GPIO-related ones — behaved differently across this transition. On Trixie, lgpio (the backend that gpiozero depends on for Pi 5 GPIO access) is installed as a system-wide package. When you create a virtual environment on Trixie for GPIO work, you need to pass --system-site-packages so the environment can see lgpio and gpiozero:

# For GPIO projects on Trixie: use --system-site-packages
# so the venv can access system-installed lgpio and gpiozero
python3 -m venv --system-site-packages weather-env

# For non-GPIO projects on either OS: a clean venv is fine
python3 -m venv weather-env

# Activate it
source weather-env/bin/activate

# Always upgrade pip first — avoids stale resolver warnings
pip install --upgrade pip

# Now pip installs are isolated to this project
pip install requests matplotlib
Pro Tip

Name your virtual environment something project-specific rather than a generic name like env. For example, a weather station project might use python3 -m venv weather-env. This keeps things readable when you have multiple projects on the same Pi.

For writing code, Thonny is the IDE bundled with Raspberry Pi OS and is a solid choice for beginners. It has a built-in Python shell, variable inspector, and direct support for running scripts on the Pi. If you prefer to work from another machine, VS Code running on the Pi 5 (which has enough power to run it comfortably) or a remote SSH session from a laptop are both popular options. You can also connect to your Pi from a phone or tablet over SSH using apps like Termux on Android or Terminus on iOS.

Installing Packages

With your virtual environment active, pip works as you would expect. A few packages that are especially useful for Raspberry Pi projects include:

  • gpiozero — high-level GPIO control, pre-installed in Raspberry Pi OS, works across all Pi models including Pi 5
  • RPi.GPIO — the classic library, works on Pi 4 and earlier; use rpi-lgpio as a drop-in on Pi 5
  • requests — HTTP calls to APIs and web services
  • Pillow — image processing, useful with the Pi Camera module
  • matplotlibplotting sensor data
  • Flask — a lightweight web framework for building dashboards accessible from any browser on your network
  • smbus2 — I2C communication for sensors like temperature and humidity modules
  • picamera2 — the current Python camera library for Raspberry Pi OS Bullseye and later; install via sudo apt install python3-picamera2, not pip
  • aiohttpasync HTTP client/server for running web tasks concurrently with hardware tasks via Python's built-in asyncio

How to Set Up Python for GPIO: Step by Step

The steps below consolidate the setup process into a single reference. Follow them in order the first time, and use them as a checklist on any new Pi deployment.

  1. Confirm your OS release and Python version

    Before installing anything, check what you are working with. The virtual environment flag you need and the GPIO libraries available depend entirely on which OS release you have.

    python3 --version
    lsb_release -a
  2. Create a virtual environment

    On Trixie, always use --system-site-packages for any project that touches GPIO — this lets the venv see the system-installed lgpio and gpiozero. On Bookworm, the flag is optional for non-GPIO projects.

    # Trixie (GPIO project)
    python3 -m venv --system-site-packages myproject-env
    
    # Bookworm (non-GPIO project, or if lgpio is pip-installable)
    python3 -m venv myproject-env
  3. Activate the venv and upgrade pip

    Activate the environment, then upgrade pip before installing any packages. Stale pip versions sometimes cause dependency resolution errors that are difficult to diagnose.

    source myproject-env/bin/activate
    pip install --upgrade pip
  4. Verify gpiozero is accessible

    Confirm the venv can see gpiozero before writing any hardware code. If this command fails on Trixie, you created the venv without --system-site-packages — delete it and recreate with that flag.

    python3 -c "import gpiozero; print(gpiozero.__version__)"
  5. Write and run your first GPIO script

    Connect an LED to BCM pin 17 through a 330-ohm resistor to ground. Create led_blink.py with the code below and run it. If the LED blinks, your environment is fully functional.

    from gpiozero import LED
    from time import sleep
    
    led = LED(17)
    
    while True:
        led.on()
        sleep(1)
        led.off()
        sleep(1)
  6. Harden SSH and enable UFW before network deployment

    Before putting the Pi on a network you do not fully control, disable SSH password authentication and set up UFW. This step is skipped in tutorials and costs people dearly. See the Security section below for the complete commands.

Bookworm vs Trixie: Which OS Are You On?

This question matters more than it might seem. Raspberry Pi OS Trixie launched in October 2025 and brought Python 3.13, a kernel 6.12 LTS base, and labwc as the default Wayland compositor. (labwc actually became the default on Bookworm in October 2024, replacing wayfire, which Raspberry Pi Ltd announced it would no longer maintain with updates. Trixie continues that direction, shipping with labwc as the only supported compositor. Wayfire can still be installed from packages, but it receives no active support from the Raspberry Pi team.) This is a meaningfully different GPIO library situation from Bookworm. If you are starting a new project today, you should know which OS you have and what it means for your Python workflow.

On Bookworm (Python 3.11), the ecosystem is stable and well-documented. The virtual environment pattern is straightforward: create a venv, activate it, install with pip. The pigpio daemon was available for projects needing precise timing. The vast majority of tutorials written between 2023 and 2025 target Bookworm.

On Trixie (Python 3.13), several things changed. The pigpio package was removed from the default OS image — it does not support the Pi 5 and Raspberry Pi Ltd is letting it retire. More importantly, lgpio (which backs gpiozero on Pi 5) is distributed as a system package. If you create a standard venv without --system-site-packages, your GPIO code will fail silently or throw import errors. The fix is one flag, but it is easy to miss. It is worth noting that at initial Trixie release, the lgpio Python package was not yet available for Python 3.13 from pip — it is accessible only as the system-installed package, which is exactly why the --system-site-packages flag matters so much on this OS release. This was confirmed by multiple users on the official Raspberry Pi forums and in the Trixie release discussion thread.

# Trixie: confirm your Python and OS version first
python3 --version   # should say 3.13.x
lsb_release -c      # should say trixie

# Create a GPIO-compatible venv on Trixie
python3 -m venv --system-site-packages myproject-env
source myproject-env/bin/activate

# Confirm gpiozero is accessible
python3 -c "import gpiozero; print(gpiozero.__version__)"
Feature Bookworm (Debian 12) Trixie (Debian 13)
Python version 3.11 3.13
Kernel 6.6 LTS 6.12 LTS
Wayland compositor labwc (from Oct 2024) labwc (only supported option)
lgpio availability pip-installable System package only (use --system-site-packages)
pigpio Available Removed — does not support Pi 5 RP1
GPIO venv flag Optional Required: --system-site-packages
Status Supported; security patches only Current — all new features here
Upgrade path n/a Clean install recommended by Raspberry Pi Ltd
Bookworm 3.11
Trixie 3.13
Bookworm 6.6 LTS
Trixie 6.12 LTS
Bookworm labwc (from Oct 2024)
Trixie labwc (only supported option)
Bookworm pip-installable
Trixie System package only — use --system-site-packages
Bookworm Available
Trixie Removed — does not support Pi 5 RP1
Bookworm Optional
Trixie Required: --system-site-packages
Bookworm Supported; security patches only
Trixie Current — all new features here
Bookworm n/a
Trixie Clean install recommended by Raspberry Pi Ltd

One more change worth knowing if you are new to camera projects: picamzero, a simplified beginner-oriented wrapper around picamera2 created by the Raspberry Pi Foundation, is available on both Bookworm and Trixie. If you are just getting started with the camera module, picamzero requires fewer lines of code than picamera2 directly, but switch to picamera2 when you need full control over capture parameters, stream configurations, or video encoding.

Bookworm is Becoming Legacy

Bookworm will continue receiving security and critical patches for at least two more years, but new features and package updates are focused on Trixie. If you are building something you plan to maintain for more than a year, starting fresh on Trixie is worth the extra hour of setup — especially if it means not having to migrate later.

Raspberry Pi Ltd stated that wayfire would no longer receive updates in Raspberry Pi OS, and recommended moving to labwc promptly. — Raspberry Pi Ltd, October 2024

One more important practical note: Raspberry Pi Ltd officially recommends a clean install via Raspberry Pi Imager when moving to Trixie, rather than an in-place upgrade from Bookworm. In-place upgrades have been tested and documented in the beta forum, but the team's stated position is that only clean images are officially supported. If you have a Pi that has been running Bookworm for a year or more, the safest path to Trixie is a fresh image, not an apt dist-upgrade.

Raspberry Pi 40-pin GPIO Header — BCM Numbering Function BCM Phys Phys BCM Function 3.3V power 1 | 2 5V power SDA (I2C) 2 3 | 4 5V power SCL (I2C) 3 5 | 6 GND GPIO 4 7 | 8 14 TX (UART) GND 9 | 10 15 RX (UART) GPIO [LED ex.] 17 11 | 12 18 GPIO / PWM GPIO 27 13 | 14 GND GPIO 22 15 | 16 23 GPIO 3.3V power 17 | 18 24 GPIO MOSI (SPI) 10 19 | 20 GND MISO (SPI) 9 21 | 22 25 GPIO 3.3V 5V GND GPIO I2C SPI UART
Partial Raspberry Pi 40-pin GPIO header map (BCM numbering). Pin 11 / BCM 17 is used in the LED example; Pin 5 / BCM 3 is used in the button example. I2C SDA and SCL are on physical pins 3 and 5. All GPIO pins operate at 3.3V logic — never connect directly to 5V sources.

Controlling GPIO Pins with Python

The 40-pin GPIO header on a Raspberry Pi exposes digital input/output pins, power pins (3.3V and 5V), ground pins, and hardware interfaces for I2C, SPI, and UART. The pins are numbered in two ways: by their physical position on the header (BOARD numbering) or by the Broadcom chip's internal numbering (BCM). The gpiozero library defaults to BCM numbering, which is what most documentation and pinout diagrams use.

Your First GPIO Script: Blinking an LED

Connect an LED to BCM pin 17 (physical pin 11) with a 330-ohm resistor in series to ground. Then write the following:

from gpiozero import LED
from time import sleep

led = LED(17)

while True:
    led.on()
    sleep(1)
    led.off()
    sleep(1)

That is the complete script. No boilerplate, no initialization ritual. gpiozero is designed to be readable first. When the script exits — either by pressing Ctrl+C or reaching the end — the library automatically cleans up the GPIO state, avoiding the common issue of pins being left in an undefined state.

Reading a Button

Connect a button between BCM pin 3 (physical pin 5) and ground. gpiozero handles the internal pull-up resistor automatically. Note that BCM pins 2 and 3 have hardware I2C pull-up resistors permanently connected — they work fine for a button here, but if you plan to use I2C at the same time, choose a different GPIO pin such as BCM pin 18 (physical pin 12):

from gpiozero import LED, Button
from signal import pause

led = LED(17)
button = Button(3)

# Wire the button press directly to LED behavior
button.when_pressed = led.on
button.when_released = led.off

# Keep the script running and listening for events
pause()

Notice that there is no loop here. The pause() call from Python's signal module keeps the script alive while gpiozero handles the event callbacks in the background. This event-driven style is one of gpiozero's most distinctive features and makes hardware interaction feel much more like modern Python than traditional C-style GPIO programming.

Protect Your Pins

GPIO pins on the Raspberry Pi operate at 3.3V logic and can only source or sink a small amount of current (typically 16mA per pin). Never connect a GPIO pin directly to 5V, and always use a current-limiting resistor with LEDs. Driving motors or relays directly from a GPIO pin will damage your Pi — use a dedicated motor driver or relay module instead.

PWM: Dimming an LED or Controlling a Servo

Pulse Width Modulation lets you simulate analog output from a digital pin by switching it on and off very rapidly. gpiozero makes PWM straightforward:

from gpiozero import PWMLED
from time import sleep

led = PWMLED(17)

# Gradually brighten, then dim
while True:
    for brightness in range(0, 101, 5):
        led.value = brightness / 100
        sleep(0.05)
    for brightness in range(100, -1, -5):
        led.value = brightness / 100
        sleep(0.05)

The same PWM principle applies to servo motors. gpiozero includes a Servo class that accepts values between -1 (full left) and 1 (full right), with 0 being center position.

CHECK YOUR UNDERSTANDING
Question 1 of 3
Q1
You connect an LED to BCM pin 17 with a 330-ohm resistor. Which gpiozero class should you instantiate to control it?
Q2
What does the pause() call in the button example actually do?
Q3
A PWMLED value of 0.5 produces what behavior?
correct on first try

Concurrent Hardware: asyncio on the Pi

One question that rarely gets addressed in beginner Pi guides is what to do when you need to do more than one thing at once — read a sensor every five seconds, serve a web dashboard, and respond to a button press, all from the same Python script. The instinctive answer is threads, but threads introduce their own complexity: race conditions, lock management, and debugging nightmares. Python's asyncio library is a cleaner answer for I/O-bound hardware tasks on the Pi.

The key insight is that most hardware interactions are I/O-bound, not CPU-bound. Waiting for a sensor reading, waiting for an HTTP response, waiting for a button event — these are all idle time from the CPU's perspective. asyncio lets you write code that cooperatively yields during that idle time, allowing other tasks to run. The result is a single-threaded script that looks and reads like synchronous code but handles multiple concurrent hardware tasks without threads:

import asyncio
from gpiozero import LED

led = LED(17)

async def blink_task():
    """Blink an LED every 2 seconds."""
    while True:
        led.on()
        await asyncio.sleep(1)
        led.off()
        await asyncio.sleep(1)

async def sensor_task():
    """Simulate reading a sensor every 5 seconds."""
    while True:
        # Replace with actual sensor read
        print("Reading sensor...")
        await asyncio.sleep(5)

async def web_task():
    """Placeholder for an aiohttp web server or MQTT publish loop."""
    while True:
        print("Web heartbeat")
        await asyncio.sleep(10)

async def main():
    await asyncio.gather(
        blink_task(),
        sensor_task(),
        web_task(),
    )

asyncio.run(main())

The await asyncio.sleep() calls are what make this work — they yield control back to the event loop so other coroutines can run. This pattern scales well: you can add an MQTT publish loop, a Flask-like web handler via aiohttp, or a watchdog timer all within the same script without touching threads. For tasks that are genuinely CPU-bound — image processing, number crunching, machine learning inference — you would use asyncio.to_thread() to hand them off to a thread pool while keeping the rest of the event loop responsive.

Pro Tip

When mixing gpiozero event callbacks (like button.when_pressed) with an asyncio event loop, be aware that gpiozero's callbacks run in a background thread. Use asyncio.get_running_loop().call_soon_threadsafe() to safely schedule coroutines from those callbacks into the asyncio event loop. (On Python 3.10+, get_running_loop() is preferred over the older get_event_loop().)

SPOT THE BUG

The script below is supposed to run two concurrent tasks using asyncio: one blinks an LED every second, the other prints a sensor reading every three seconds. It has one bug that will cause it to fail silently — the tasks will not run concurrently at all. Find it.

import asyncio
from gpiozero import LED

led = LED(17)

async def blink_task():
    while True:
        led.on()
        await asyncio.sleep(0.5)
        led.off()
        await asyncio.sleep(0.5)

async def sensor_task():
    while True:
        print("Sensor reading: 22.4C")
        await asyncio.sleep(3)

async def main():
    await blink_task()     # line A
    await sensor_task()    # line B

asyncio.run(main())

Reading Sensor Data

Where GPIO output lets your Pi affect the world, GPIO input lets it perceive the world. The Raspberry Pi's header exposes hardware I2C and SPI buses, making it straightforward to connect a wide range of sensors.

DHT11 / DHT22: Temperature and Humidity

The DHT series sensors use a single-wire protocol and are among the most commonly used sensors in Pi projects. Install the adafruit-circuitpython-dht library and connect the sensor's data pin to a free GPIO:

import adafruit_dht
import board
import time

# use_pulseio=False is required on Raspberry Pi OS (Bookworm and Trixie).
# The default pulseio backend requires libgpiod2, which changed ABI in Trixie.
# Passing use_pulseio=False uses the kernel's gpiod interface directly instead.
dht_sensor = adafruit_dht.DHT22(board.D4, use_pulseio=False)

while True:
    try:
        temperature = dht_sensor.temperature
        humidity = dht_sensor.humidity
        print(f"Temperature: {temperature:.1f}C  Humidity: {humidity:.1f}%")
    except RuntimeError as error:
        # DHT sensors occasionally miss a reading — this is normal; just retry
        print(f"Reading error: {error.args[0]}")
    except Exception as error:
        # Unexpected error: clean up the sensor process before re-raising
        dht_sensor.exit()
        raise error
    time.sleep(2.0)
Note

The DHT11 and DHT22 sensors are inexpensive but inherently unreliable — they miss readings regularly. Always wrap reads in a try/except block and either skip or retry on error. The DHT22 is more accurate (0.5C vs 2C) and worth the small additional cost for projects where precision matters.

I2C Sensors

I2C is a two-wire bus (SDA on pin 3, SCL on pin 5) that allows multiple sensors to share the same two wires, each addressed uniquely. The BMP280 barometric pressure sensor is a popular I2C device. Enable I2C in Raspberry Pi Configuration first, then install the required libraries:

pip install smbus2 pimoroni-bmp280

(This example uses Pimoroni's BMP280 library, which works well with Pimoroni breakout boards. If you are using a non-Pimoroni BMP280 module, adafruit-circuitpython-bmp280 is a well-maintained alternative that works with any BMP280 breakout regardless of manufacturer.)

Then read the sensor with:

import smbus2
import bmp280

# Open the I2C bus
bus = smbus2.SMBus(1)

# Initialize BMP280 at default I2C address 0x76
sensor = bmp280.BMP280(i2c_dev=bus)

print(f"Temperature: {sensor.get_temperature():.2f} C")
print(f"Pressure:    {sensor.get_pressure():.2f} hPa")

Storing and Visualizing Sensor Data

A sensor reading is only as useful as what you do with it. A common pattern is to log readings to a CSV file and then plot them with matplotlib:

import csv
import datetime
import time
import matplotlib.pyplot as plt

LOG_FILE = "sensor_log.csv"

def log_reading(temperature: float, humidity: float) -> None:
    # Use UTC timestamps — avoids DST ambiguity in long-running logs
    ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
    with open(LOG_FILE, "a", newline="") as f:
        writer = csv.writer(f)
        writer.writerow([ts, temperature, humidity])

def plot_log() -> None:
    timestamps, temps, humidities = [], [], []
    with open(LOG_FILE, "r") as f:
        reader = csv.reader(f)
        for row in reader:
            timestamps.append(row[0])
            temps.append(float(row[1]))
            humidities.append(float(row[2]))

    fig, ax1 = plt.subplots()
    ax1.set_xlabel("Time")
    ax1.set_ylabel("Temperature (C)", color="tab:red")
    ax1.plot(temps, color="tab:red")

    ax2 = ax1.twinx()
    ax2.set_ylabel("Humidity (%)", color="tab:blue")
    ax2.plot(humidities, color="tab:blue")

    plt.title("Sensor Log")
    plt.tight_layout()
    plt.savefig("sensor_chart.png")
    print("Chart saved to sensor_chart.png")

if __name__ == "__main__":
    plot_log()

Practical Project Ideas

The hardware-plus-Python combination on the Raspberry Pi opens up a broad range of projects. Here are several well-suited to the platform at different skill levels.

Weather Station

Connect a temperature, humidity, and barometric pressure sensor to the Pi, log readings every five minutes, and serve the data as a web page using Flask. Add a small OLED display to show current readings without needing a monitor. This project covers sensors, I2C, data logging, and basic web serving — a solid foundation for many other projects.

Home Automation Controller

Use the Pi's GPIO to control a relay module, which in turn switches mains-voltage devices on and off. A Python script can respond to button presses, scheduled times, or HTTP requests from a phone. Combined with a motion sensor (PIR), you can build a presence-based lighting controller entirely in Python.

Why Python and GPIO?

The Raspberry Pi's GPIO header, combined with a full Linux environment, puts the physical world within reach of ordinary Python code. Python has been central to the platform since its earliest days, and the result is an ecosystem where hardware interactions can be expressed in terms close to human intent rather than low-level register manipulation.

Security Camera with Motion Detection

The Raspberry Pi Camera Module 3 connects directly to either of the two CSI/MIPI connectors on the Pi 5 board. The picamera2 library (the current standard as of Raspberry Pi OS Bullseye and later) gives you full control over capture parameters. Install it via apt rather than pip — this ensures you get versions of picamera2 and the underlying libcamera libraries that have been confirmed to work together:

sudo apt install python3-picamera2

Then combine it with OpenCV for motion detection:

import cv2
from picamera2 import Picamera2
import time

picam2 = Picamera2()
# BGR888 tells libcamera to output 24-bit BGR — the native format OpenCV expects.
# The default XBGR8888 is 4-channel (32-bit) and causes cv2.cvtColor to fail.
config = picam2.create_preview_configuration(
    main={"size": (640, 480), "format": "BGR888"}
)
picam2.configure(config)
picam2.start()

background = None

try:
    while True:
        frame = picam2.capture_array()
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (21, 21), 0)

        if background is None:
            background = gray
            continue

        delta = cv2.absdiff(background, gray)
        thresh = cv2.threshold(delta, 25, 255, cv2.THRESH_BINARY)[1]
        contours, _ = cv2.findContours(
            thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )

        for contour in contours:
            if cv2.contourArea(contour) > 500:
                print(f"Motion detected at {time.strftime('%H:%M:%S')}")
                # Save frame, send notification, trigger relay, etc.
                break

        time.sleep(0.1)
finally:
    picam2.close()
Pro Tip

Use systemd to run your Python scripts automatically on boot. Create a service file in /etc/systemd/system/, enable it with sudo systemctl enable your-script.service, and your Pi becomes a truly standalone appliance — no keyboard or monitor required after initial setup.

Network-Connected Data Logger

Send sensor readings to a cloud service using Python's requests library. Services like ThingSpeak, InfluxDB Cloud, or a simple home server running InfluxDB + Grafana accept HTTP POST requests with JSON payloads. This lets you build long-term sensor histories and dashboards viewable from anywhere.

import datetime
import requests

WEBHOOK_URL = "https://your-server.example.com/api/readings"

def send_reading(temperature: float, humidity: float) -> None:
    payload = {
        "temperature": temperature,
        "humidity": humidity,
        # ISO 8601 UTC string — more portable than a raw Unix float
        "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    }
    try:
        response = requests.post(WEBHOOK_URL, json=payload, timeout=5)
        response.raise_for_status()   # raises HTTPError for 4xx/5xx responses
        print("Reading sent successfully")
    except requests.exceptions.HTTPError as e:
        print(f"Server error: {e.response.status_code}")
    except requests.exceptions.RequestException as e:
        print(f"Network error: {e}")

Securing Your Pi Before It Goes Online

This question is almost never asked in Pi Python tutorials, and that is a problem. A Pi running a data logger, home automation controller, or camera server is often a permanently-connected device on your home or office network. Leaving it with default settings is the network equivalent of leaving your front door unlocked. If you want to go further than the basics covered here, the guide to building Python security automation tools — including port scanners and log analyzers — is a natural next step for a Pi that is permanently on your network.

Raspberry Pi OS Trixie made meaningful improvements here: new installations no longer default to the familiar pi username with a well-known default password. SSH is disabled by default and must be explicitly enabled. These are good baseline changes, but they are not sufficient for a Pi you plan to leave running for months. Here are the things your Python-focused tutorials will skip but you should not:

Change the default password and username immediately. If you created your image with Raspberry Pi Imager, you were prompted to set these. If you inherited a Pi from a tutorial or used an old image, run passwd and create a new user to replace pi.

Use SSH keys, not passwords. Generate a key pair on your laptop, copy the public key to the Pi with ssh-copy-id, then disable password authentication in /etc/ssh/sshd_config by setting PasswordAuthentication no. A brute-force attack against SSH key authentication is computationally infeasible. Against a password, it is just a matter of time.

Install and configure UFW (Uncomplicated Firewall). By default, every port your Pi is listening on is accessible from any device on your network. UFW lets you define exactly which ports are open:

sudo apt install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
# If you're running a Flask dashboard on port 5000:
sudo ufw allow 5000/tcp
sudo ufw enable
sudo ufw status

Keep the system updated automatically. A Pi sitting unattended for months will fall behind on security patches. Install unattended-upgrades and configure it to apply security updates automatically:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

If your Pi is exposed to the internet — for example via port forwarding to access your dashboard remotely — the risk profile is substantially higher. In that case, consider placing it behind a VPN (WireGuard runs well on the Pi 5) rather than exposing services directly. Your Python project data is worth protecting.

SD Cards, SSDs, and Long-Running Pi Projects

Here is something almost no beginner tutorial covers: if you run a Pi data logger that writes sensor readings to a CSV file every five minutes, and your Pi is powered on continuously, you will eventually destroy your SD card. SD cards have a finite number of write cycles per memory cell. Constant small writes — the pattern produced by data logging, database writes, and even heavy system logging — accelerate wear dramatically. A mid-grade SD card running a logger that writes every five seconds can fail in a matter of months.

The solutions range from simple software changes to hardware upgrades. Each addresses a different part of the wear problem, and combining them matters more than picking just one:

Batch your writes. Instead of writing to disk every sensor reading, accumulate readings in memory and write them in batches every 15 or 30 minutes. This is also good Python practice — file I/O is expensive, and batching reduces the frequency dramatically.

Switch from CSV to SQLite with WAL mode. A Python script that opens and writes to a CSV file on every reading has especially high overhead — each write involves a filesystem open, a seek to end-of-file, and a close. SQLite with WAL (Write-Ahead Logging) enabled batches writes internally and reduces the number of actual disk syncs considerably. Enabling WAL is a single pragma call: PRAGMA journal_mode=WAL;. For a data logger that runs for months, this change alone can meaningfully extend SD card life compared to unbatched CSV writes. It also gives you queryable history without any additional tooling.

Reduce log verbosity. Raspberry Pi OS's default system logging configuration writes constantly to /var/log/syslog, /var/log/kern.log, and other files. For a headless Pi that will run unattended, you can configure rsyslog to raise the minimum severity level so only warnings and errors are written, or disable specific facility-severity combinations that have no value in your deployment. A Pi that is not routing traffic or running a mail server has no operational reason to log every INFO-level system event to disk. Edit /etc/rsyslog.conf and set your relevant facilities to warn rather than info or debug.

Disable swap on a low-write-load Pi. Swap on an SD card is a known wear accelerator. If your Pi project uses less than its available RAM (many sensor loggers do), swap activity adds write pressure with no benefit. You can disable swap entirely with sudo dphys-swapfile swapoff && sudo dphys-swapfile uninstall && sudo systemctl disable dphys-swapfile. For a Pi 5 with 4 or 8 GB of RAM running a data logger, swap is rarely needed and safe to disable.

Use a high-endurance SD card as a baseline. Most SD cards sold for consumer cameras are rated for roughly 1,000 to 3,000 Program/Erase cycles per cell. Industrial and high-endurance variants (such as the Sandisk Endurance series or the official Raspberry Pi SD card) are rated significantly higher — typically 10,000–40,000 P/E cycles — and are designed for continuous write workloads like dashcam and surveillance recording. Using one does not eliminate wear, but it raises the ceiling substantially for deployments where you cannot use an SSD.

import csv
import datetime
from typing import TypedDict

BATCH_SIZE = 30       # flush to disk every 30 readings
LOG_FILE   = "sensor_log.csv"

class Reading(TypedDict):
    time: str
    temp: float
    humidity: float

# Module-level buffer — declared explicitly so callers know it's shared state
_buffer: list[Reading] = []

def log_reading(temperature: float, humidity: float) -> None:
    _buffer.append({
        "time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        "temp": temperature,
        "humidity": humidity,
    })
    if len(_buffer) >= BATCH_SIZE:
        flush_buffer()

def flush_buffer() -> None:
    if not _buffer:
        return
    with open(LOG_FILE, "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["time", "temp", "humidity"])
        writer.writerows(_buffer)
    count = len(_buffer)
    _buffer.clear()
    print(f"Flushed {count} readings to disk")

Boot from an SSD. The Raspberry Pi 5 has a PCIe interface via its FPC connector, and with a HAT-format NVMe base board, you can boot an NVMe SSD directly. The connection is certified at PCIe Gen 2 (5 GT/sec), but Gen 3 (8 GT/sec, roughly double the bandwidth) can be enabled by adding dtparam=pciex1_gen=3 to /boot/firmware/config.txt. Raspberry Pi does not certify Gen 3 speeds and notes that connections may be unstable — in practice, many drives work fine at Gen 3, but results vary depending on the SSD model and the quality of your FPC ribbon cable. Start with Gen 2 and only try Gen 3 once your setup is confirmed stable. Real-world benchmarks (documented by Jeff Geerling and others) show approximately 390–450 MB/s sequential read at Gen 2 and 780–900 MB/s at Gen 3 on compatible drives, compared to 40–100 MB/s for the Pi 5's SD card slot. NVMe SSDs also have vastly more write endurance than SD cards and are far more resistant to corruption if power is cut unexpectedly. For any Pi that will run continuously for more than a few months, an SSD is genuinely the right choice, not a luxury upgrade. See: Jeff Geerling's Gen 3 benchmarks.

Use a RAM-backed filesystem for temporary data. Raspberry Pi OS includes tmpfs support. Mounting /tmp and /var/log to RAM means high-frequency writes go to RAM, not the SD card. Your Python scripts can write intermediate data to /tmp and only flush important results to the SD card or SSD periodically. Add these lines to /etc/fstab:

tmpfs   /tmp         tmpfs  defaults,noatime,nosuid,size=100m    0 0
tmpfs   /var/log     tmpfs  defaults,noatime,nosuid,size=30m     0 0

None of these are exotic solutions. They are the kind of thing that experienced Pi deployers do automatically, and the kind of thing that costs someone a Pi and months of logged data when they find out the hard way.

Key Takeaways

  1. Know which OS you are running: Bookworm (Python 3.11) and Trixie (Python 3.13) behave differently for GPIO projects. On Trixie, create virtual environments with --system-site-packages to access system-installed lgpio. For new projects, Trixie is the forward-looking choice.
  2. Use gpiozero for new GPIO projects: It works across all Raspberry Pi models including the Pi 5, is pre-installed in Raspberry Pi OS, and requires far less code than lower-level alternatives. If you have existing RPi.GPIO code to migrate to a Pi 5, the rpi-lgpio drop-in replacement handles compatibility without rewrites. Note that pigpio was removed from the Trixie image because it does not support the Pi 5's RP1 chip — if your project depends on it, stay on Bookworm or migrate to gpiozero, which covers the same use cases on current hardware.
  3. Always use virtual environments: Both Bookworm and Trixie enforce PEP 668, preventing system-wide pip installs. A virtual environment per project keeps dependencies isolated and prevents conflicts — essential when a Pi runs multiple long-term projects simultaneously.
  4. The Pi is not a microcontroller: It runs Linux, which means it is not suitable for timing-critical tasks that need microsecond precision. For those cases, pair your Pi with a Raspberry Pi Pico (which runs MicroPython and provides real-time control) via USB or UART, letting each device do what it does best.
  5. Protect your hardware and your network: GPIO pins operate at 3.3V and have strict current limits. Use resistors with LEDs, level shifters for 5V devices, and proper driver ICs for motors and relays. And before you connect that Pi to your network and walk away, lock down SSH, enable UFW, and enable automatic security updates.
  6. Plan your storage before you deploy: SD cards wear out under constant write pressure. For data logging projects, batch your writes, switch from CSV to SQLite with WAL mode, reduce system log verbosity, disable swap if your project does not need it, and use a high-endurance SD card as a baseline. For any deployment expected to run more than a few months, consider booting from an NVMe SSD on Pi 5 — real-world benchmarks show Gen 2 delivering roughly 390–450 MB/s sequential read; Gen 3 roughly doubles that to 780–900 MB/s depending on the drive. Gen 3 is accessible via a single config line but is not officially certified — verify stability with your specific drive before relying on it in production.
  7. Run long-running scripts as systemd services: For anything that needs to start on boot and keep running — a camera monitor, data logger, or home automation controller — systemd services are the right tool. They restart automatically on failure and log output to the system journal.

The Raspberry Pi and Python represent one of the most accessible on-ramps into hardware programming that has ever existed. Building a networked sensor station with a web dashboard and motion-triggered camera once required significant electronics knowledge or a large budget. Today it takes an afternoon, a handful of inexpensive parts, and the Python skills you are already building. The physical world is just another API — and Python gives you the client library to call it. The only difference between a hobbyist project and a production system is everything this article covered after the blinking LED.

If this guide helped clarify something that other python tutorials tend to skip — the OS differences, the asyncio pattern, the storage gotchas, the security steps nobody mentions — that is exactly the goal. Hardware programming with Python rewards the people who understand what is actually happening, not just the people who copy the first snippet they find.

Frequently Asked Questions

  • No. RPi.GPIO does not work on the Raspberry Pi 5 because the Pi 5 uses a new RP1 I/O chip with a different memory mapping. Use gpiozero for new projects — it is pre-installed in Raspberry Pi OS and works on all Pi models including the Pi 5. If you have existing RPi.GPIO code, the rpi-lgpio drop-in replacement package on PyPI restores compatibility without requiring code changes.

  • Yes. Both Raspberry Pi OS Bookworm and Trixie enforce PEP 668, which marks the base Python environment as externally managed and blocks system-wide pip installs. You must create a virtual environment (python3 -m venv myenv) before installing packages with pip. On Trixie, GPIO projects additionally require the --system-site-packages flag so the venv can access the system-installed lgpio package.

  • Bookworm ships Python 3.11 and a 6.6 LTS kernel. Trixie, released October 2025, ships Python 3.13 and a 6.12 LTS kernel. The key practical difference for GPIO development is that on Trixie, lgpio is a system-only package — you must create virtual environments with --system-site-packages to access it. pigpio was removed from Trixie because it does not support the Pi 5's RP1 chip. Raspberry Pi Ltd recommends a clean install when moving to Trixie rather than an in-place upgrade.

  • Use systemd. Create a service file in /etc/systemd/system/ that specifies your Python interpreter path and script location, then run sudo systemctl enable your-script.service and sudo systemctl start your-script.service. systemd will start the script on every boot, restart it automatically if it crashes, and route its output to the system journal accessible via journalctl.

  • Yes, potentially. SD cards have a finite number of write cycles per memory cell. A script that writes sensor readings to disk every few seconds can degrade a mid-grade SD card within months of continuous operation. Mitigations include batching writes (accumulate readings in memory and flush every 15–30 minutes), mounting /tmp and /var/log to tmpfs (a RAM-backed filesystem), and for long-running deployments on the Pi 5, booting from an NVMe SSD via the PCIe FPC connector.

  • Yes, but with one important caveat: gpiozero event callbacks (such as button.when_pressed) run in a background thread, not in the asyncio event loop. If you need to trigger async code from a gpiozero callback, use asyncio.get_running_loop().call_soon_threadsafe() to safely schedule coroutines from that background thread into the event loop. For non-callback tasks like reading sensors at intervals, asyncio.gather() with await asyncio.sleep() works cleanly alongside gpiozero.

  • Install picamera2 via apt, not pip: run sudo apt install python3-picamera2. Installing via pip risks pulling in a version of libcamera that conflicts with your OS release. After installation, picamera2 is available system-wide. If you are working inside a virtual environment, create it with --system-site-packages so the venv can see the system-installed picamera2.

Sources and Further Reading