To run a Python script on a Raspberry Pi, the immediate answer is to execute it via the terminal using python3 your_script.py. However, for any persistent IoT or automation project, you need the script to run automatically at boot, survive network drops, and interface safely with the GPIO header.

This guide targets the Raspberry Pi 5 (8GB variant) running the latest 64-bit Raspberry Pi OS (Bookworm). The Pi 5 introduces the RP1 southbridge chip, which fundamentally changes how GPIO and I2C are handled at the kernel level, rendering legacy libraries like RPi.GPIO obsolete. We will build a robust environmental monitor using modern, supported tools.

Hardware Spec Sheet & Parts List

Before writing code, verify your hardware. The Pi 5 has stricter power requirements than its predecessors; running peripherals on an underpowered supply will cause random I2C bus drops and kernel panics.

Component Exact Variant / Specification Notes
Microcontroller Raspberry Pi 5 (8GB RAM) Requires 27W (5V/5A) USB-C PD power supply for full peripheral current.
Sensor Adafruit BME280 I2C Breakout Default I2C address 0x77 (or 0x76 if pad is bridged).
Indicator 5mm Diffused Red LED Forward voltage ~2.0V.
Resistor 330Ω (1/4W) Limits LED current to ~10mA from the 3.3V logic pin.
Wiring 22 AWG Silicone Jumper Wires Female-to-female for Pi header to breadboard.
Power Warning: The Raspberry Pi 5 limits USB and GPIO current by default if it does not detect a 5A PD power supply. If your I2C sensor fails to initialize or the Pi reboots under load, verify your power supply using vcgencmd get_throttled in the terminal.

Pin Mapping & Physical Wiring

We are using BCM (Broadcom) pin numbering, which is the standard for modern Python libraries like gpiozero. The Pi 5 routes the primary I2C bus through the RP1 chip, but the physical header pins remain identical to the Pi 4 for backward compatibility.

Function BCM Pin Physical Pin Connected To
3.3V Power N/A 1 BME280 VIN
Ground N/A 6 BME280 GND & LED Cathode
I2C SDA 2 3 BME280 SDI/SDA
I2C SCL 3 5 BME280 SCK/SCL
GPIO Output 17 11 330Ω Resistor → LED Anode

Wiring Steps

  1. Disconnect power from the Raspberry Pi 5.
  2. Connect Physical Pin 1 (3.3V) to the BME280 VIN (or VCC) pin.
  3. Connect Physical Pin 6 (GND) to the BME280 GND pin and the short leg (cathode) of the LED.
  4. Connect Physical Pin 3 (SDA) to BME280 SDA, and Physical Pin 5 (SCL) to BME280 SCL.
  5. Connect Physical Pin 11 (GPIO 17) to the 330Ω resistor, then connect the other end of the resistor to the long leg (anode) of the LED.
  6. Power on the Pi and boot into the desktop or SSH session.

The Python Script: Reading I2C with Error Handling

Raspberry Pi OS (Bookworm) enforces PEP 668, meaning you cannot globally pip install packages without breaking system dependencies. We will use a Python virtual environment.

Run these commands in your terminal to set up the environment and install the required libraries (gpiozero for the LED, and Adafruit's Blinka/BME280 libraries for the sensor):

mkdir ~/env_monitor && cd ~/env_monitor
python3 -m venv venv
source venv/bin/activate
pip install gpiozero adafruit-blinka adafruit-circuitpython-bme280 lgpio

Next, create a file named monitor.py and paste the following complete, compilable code. This script includes explicit pin definitions, hardware initialization checks, and a finally block to ensure the GPIO pin is safely driven low if the script crashes.

import time
import sys
import board
import busio
import adafruit_bme280
from gpiozero import LED

# --- PIN DEFINITIONS (BCM Numbering) ---
LED_PIN = 17  # Physical Pin 11
I2C_ADDRESS = 0x76  # Change to 0x77 if your BME280 breakout is unmodified

# Initialize GPIO
status_led = LED(LED_PIN)

# Initialize I2C and Sensor
try:
    i2c = busio.I2C(board.SCL, board.SDA)
    bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=I2C_ADDRESS)
    print("Hardware initialized successfully.")
except ValueError as e:
    print(f"Hardware Init Error: {e}")
    sys.exit(1)
except Exception as e:
    print(f"Unexpected I2C Bus Error: {e}")
    sys.exit(1)

try:
    while True:
        temp_c = bme280.temperature
        humidity = bme280.relative_humidity
        print(f"Temp: {temp_c:.2f} C | Humidity: {humidity:.1f} %")
        
        # Blink LED once per read cycle
        status_led.blink(on_time=0.5, off_time=0.5, n=1, background=False)
        time.sleep(10)

except KeyboardInterrupt:
    print("\nScript terminated by user (Ctrl+C).")
except Exception as e:
    print(f"\nRuntime Error during loop: {e}")
finally:
    # Ensure LED is turned off and resources are released
    status_led.off()
    print("GPIO cleaned up. Exiting safely.")

Running the Script Automatically at Boot

Running a script via rc.local or .bashrc is unreliable for modern embedded projects. The correct approach on systemd-based Linux (which Raspberry Pi OS uses) is to create a custom service unit. This ensures your script restarts if it crashes and logs output to the system journal.

  1. Open a terminal and create a new service file: sudo nano /etc/systemd/system/envmonitor.service
  2. Paste the following configuration. Note the explicit paths to the virtual environment's Python binary and your script:
[Unit]
Description=Environmental Monitor Python Script
After=network.target

[Service]
ExecStart=/home/pi/env_monitor/venv/bin/python3 /home/pi/env_monitor/monitor.py
WorkingDirectory=/home/pi/env_monitor
StandardOutput=append:/home/pi/env_monitor/monitor.log
StandardError=append:/home/pi/env_monitor/monitor_error.log
Restart=always
RestartSec=10
User=pi

[Install]
WantedBy=multi-user.target
  1. Save and exit (Ctrl+O, Enter, Ctrl+X).
  2. Reload the systemd daemon to recognize the new file: sudo systemctl daemon-reload
  3. Enable the service to run at boot: sudo systemctl enable envmonitor.service
  4. Start it immediately to test: sudo systemctl start envmonitor.service
Pro Tip: To view live logs from your background service without opening the text files, use the journalctl command: journalctl -u envmonitor.service -f. This is invaluable for debugging I2C timeouts over SSH.

Debugging: First 3 Checks & Exact Error Strings

When your script fails to run, do not guess. Follow this strict diagnostic path. The first three things to check when it fails are:

  1. Verify I2C is enabled: Run sudo raspi-config → Interface Options → I2C → Enable. The RP1 chip on the Pi 5 requires this to map the /dev/i2c-1 device tree overlay.
  2. Verify physical addressing: Run i2cdetect -y 1. If your sensor address (e.g., 0x76) does not show up in the grid, you have a wiring fault or a missing pull-up resistor (the Adafruit breakout has them built-in, but generic clones often do not).
  3. Verify the virtual environment: Ensure your systemd ExecStart points to /home/pi/env_monitor/venv/bin/python3, not the global /usr/bin/python3, or it will fail to find the Adafruit libraries.

Exact Error Strings and Ranked Causes

Error 1: FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1'

  • Cause A (Most Likely): I2C interface is disabled in raspi-config.
  • Cause B: You are running a minimal headless OS image where the I2C kernel module (i2c-dev) is blacklisted or missing. Fix by running sudo modprobe i2c-dev.

Error 2: ValueError: No I2C device at address: 0x77

  • Cause A (Most Likely): Address mismatch. Most Adafruit BME280 breakouts default to 0x77, while generic Amazon/eBay clones default to 0x76. Check the silk screen on the PCB and update the I2C_ADDRESS variable in the code.
  • Cause B: SDA and SCL wires are swapped. I2C will silently fail to negotiate if the clock and data lines are reversed.

Error 3: error: externally-managed-environment (When trying to pip install)

  • Cause A (Only Cause): You are trying to install packages globally on Raspberry Pi OS Bookworm. This is blocked by PEP 668 to prevent breaking OS utilities. You must use a virtual environment (python3 -m venv) or pipx as shown in the setup steps.

Frequently Asked Questions

How do I run a Python script on Raspberry Pi without a monitor?

Enable SSH via the Raspberry Pi Imager before flashing your SD card, or place an empty file named ssh (no extension) in the root directory of the boot partition. Once connected via SSH, use tmux or screen to run scripts interactively so they survive terminal disconnects, or rely on the systemd method detailed above for permanent headless operation.

Why does my script run in Thonny but fail in the terminal?

Thonny often uses its own bundled Python environment or bypasses PEP 668 restrictions. When you run the script in the standard terminal, it uses the system Python, which enforces strict package isolation. Always test your code in the terminal using the exact virtual environment binary (./venv/bin/python3 monitor.py) that your systemd service will use to catch dependency errors before deploying.

How can I simplify this build for a basic test?

If you do not have an I2C sensor on hand, strip the code down to a simple GPIO loop to verify your pin mappings. Remove the busio and adafruit_bme280 imports, delete the I2C initialization block, and change the while loop to simply toggle the LED: status_led.toggle() followed by time.sleep(1). This isolates GPIO hardware faults from I2C bus faults.

How do I extend this to log data to the cloud?

To push data to a dashboard like Home Assistant or AWS IoT, integrate the paho-mqtt library. Inside the while loop, format your sensor readings into a JSON payload and publish it to an MQTT broker. For edge cases where the WiFi drops, implement a local SQLite3 database fallback that caches readings and flushes them to the broker once the network interface recovers. Refer to the official Raspberry Pi hardware documentation for thermal throttling limits if you are running heavy network encryption on the Pi 5.