The Definitive Decision: Which Programming Language for Raspberry Pi?
Stop guessing and debating. If you are using a Raspberry Pi Single Board Computer (SBC) like the Pi 4, Pi 5, or Pi Zero 2 W, the definitive programming language is Python 3. If you are using a Raspberry Pi Pico microcontroller (RP2040 or RP2350), the answer is C++ via the Arduino framework.
While you can run Rust, Node.js, or Go on a Pi SBC, Python remains the undisputed king for hardware integration due to the gpiozero, smbus2, and spidev libraries, which map directly to the Pi's silicon. For the Pi 5 specifically, the new RP1 southbridge chip handles GPIO routing, and the official Python wrappers are the first to receive updates for this new architecture.
Language Decision Tree
| Language | Target Hardware | Primary Strength | Fatal Weakness | Verdict |
|---|---|---|---|---|
| Python 3 | Pi 4, Pi 5, Zero 2 W | Massive library ecosystem, fast dev cycle | GIL limits true multi-threading | DEFAULT PICK for SBCs |
| C++ (Arduino) | Pico W, Pico 2 | Bare-metal speed, RTOS support, low memory | Manual memory management, slower dev | DEFAULT PICK for MCUs |
| Rust | Pico, Pi SBC | Memory safety, zero-cost abstractions | Steep learning curve, borrow checker friction | Choose for mission-critical/enterprise |
| Node.js | Pi 4, Pi 5 | Async I/O, native JSON, WebSocket ease | High RAM overhead, poor GPIO latency | Choose only if building heavy web dashboards |
Project Build: I2C BME280 MQTT Telemetry on Raspberry Pi 5
To prove why Python is the superior choice for Pi SBCs, we are building an environmental telemetry node. This script reads temperature, humidity, and pressure from a Bosch BME280 sensor over I2C and publishes it to an MQTT broker.
Target Board Variant: Raspberry Pi 5 (8GB RAM) running Raspberry Pi OS "Bookworm" (64-bit). Note: The Pi 5 requires a 27W USB-C PD power supply to prevent peripheral brownouts when polling I2C sensors and running WiFi simultaneously.
Parts List
- SBC: Raspberry Pi 5 (8GB variant, SKU SC1112) - ~$80
- Cooling: Official Raspberry Pi Active Cooler (dual fan) - ~$5
- Power: Official 27W USB-C PD Power Supply (Required for Pi 5 full peripheral current) - ~$12
- Sensor: Adafruit BME280 I2C Breakout (Product ID 2652) - ~$10
- Wiring: Female-to-Female jumper wires (20cm, 28 AWG)
Pin Mapping Table
The Pi 5's I2C bus 1 is routed through the RP1 southbridge. The physical pins remain identical to the Pi 4, but the internal pull-up resistors are now managed by the RP1 chip.
| Raspberry Pi 5 GPIO | Physical Pin # | Function | BME280 Breakout Pin |
|---|---|---|---|
| 3.3V Power | Pin 1 | VCC / VIN | VIN |
| Ground | Pin 6 | GND | GND |
| GPIO 2 | Pin 3 | I2C SDA | SDI (SDA) |
| GPIO 3 | Pin 5 | I2C SCL | SCK (SCL) |
Environment Setup and Complete Python Code
Raspberry Pi OS "Bookworm" enforces PEP 668, which marks the system Python environment as "externally managed." You can no longer use sudo pip install globally without breaking system tools. You must use a virtual environment.
Step 1: Environment and Dependencies
Run these commands in your Pi terminal to enable I2C and set up the virtual environment:
sudo raspi-config nonint do_i2c 0
sudo apt update && sudo apt install -y python3-venv python3-pip i2c-tools
mkdir ~/pi-telemetry && cd ~/pi-telemetry
python3 -m venv venv
source venv/bin/activate
pip install smbus2 bme280 paho-mqtt
Step 2: The Python Script
Save the following code as telemetry.py. This script includes explicit pin/bus definitions, hardware connection verification, and robust error handling for both the I2C bus and the MQTT network stack.
import time
import sys
import smbus2
import bme280
import paho.mqtt.client as mqtt
from paho.mqtt import MQTTException
# --- Hardware & Network Definitions ---
I2C_BUS_ID = 1 # Pi 5 uses I2C bus 1 on GPIO 2/3
BME280_ADDRESS = 0x77 # Adafruit breakout defaults to 0x77 (some clones use 0x76)
MQTT_BROKER = "192.168.1.100"
MQTT_PORT = 1883
MQTT_TOPIC = "home/lab/environment"
def on_connect(client, userdata, flags, rc, properties=None):
if rc == 0:
print("[MQTT] Connected to broker successfully.")
else:
print(f"[MQTT] Connection failed with code: {rc}")
def main():
# 1. Initialize I2C Bus
try:
bus = smbus2.SMBus(I2C_BUS_ID)
# Load calibration parameters from the sensor's non-volatile memory
calibration_params = bme280.load_calibration_params(bus, BME280_ADDRESS)
print("[I2C] BME280 initialized successfully.")
except FileNotFoundError:
print("[FATAL] I2C bus not found. Did you enable I2C in raspi-config and reboot?")
sys.exit(1)
except OSError as e:
print(f"[FATAL] I2C Hardware Error: {e}")
sys.exit(1)
# 2. Initialize MQTT Client (Paho v2.0 API)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="Pi5_BME280")
client.on_connect = on_connect
try:
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start()
except Exception as e:
print(f"[WARNING] MQTT Broker unreachable at startup: {e}")
# 3. Main Telemetry Loop
print("[INFO] Starting telemetry loop. Press Ctrl+C to exit.")
try:
while True:
try:
# Read sensor data
data = bme280.sample(bus, BME280_ADDRESS, calibration_params)
payload = (
f"{{\"temp_c\": {data.temperature:.2f}, "
f"\"humidity\": {data.humidity:.2f}, "
f"\"pressure_hpa\": {data.pressure:.2f}}}"
)
# Publish to MQTT
result = client.publish(MQTT_TOPIC, payload)
if result.rc != mqtt.MQTT_ERR_SUCCESS:
print(f"[MQTT] Publish failed: {mqtt.error_string(result.rc)}")
else:
print(f"[TX] {payload}")
except OSError as e:
print(f"[ERROR] I2C Read Failed: {e}. Check wiring.")
time.sleep(10) # Poll every 10 seconds
except KeyboardInterrupt:
print("\n[INFO] Halting telemetry.")
finally:
client.loop_stop()
client.disconnect()
bus.close()
if __name__ == "__main__":
main()
Debugging: "OSError: [Errno 121] Remote I/O error"
When working with I2C on Linux, you will inevitably encounter the dreaded OSError: [Errno 121] Remote I/O error. This is not a Python bug; it is the Linux kernel's i2c-dev driver telling you that the hardware ACK (acknowledge) bit was not received on the 9th clock cycle.
Ranked Causes
- Wrong I2C Address (Most Common): The Bosch BME280 datasheet specifies two possible addresses:
0x76(SDO tied to GND) and0x77(SDO tied to VCC). Adafruit breakouts default to 0x77, but cheap Amazon clones often default to 0x76. - SDA/SCL Swapped: I2C is not symmetric. If you swap GPIO 2 and GPIO 3, the clock line will idle high, but the data line will fail to register the start condition.
- Missing Pull-Up Resistors: I2C is an open-drain protocol. It requires pull-up resistors to pull the lines high. While the Pi 5's RP1 chip has internal pull-ups, they are weak (~50kΩ). If your wires are longer than 30cm, the bus capacitance will smear the signal edges, causing the Pi to read garbage and throw Errno 121.
1. Run
i2cdetect -y 1 in the terminal. If you see a grid of dashes and no numbers, your wiring or power is dead. If you see 77, your address is correct. If you see 76, change the BME280_ADDRESS variable in the code.2. Put a multimeter on the BME280 VIN pin. It must read exactly 3.3V. If it reads 5V, you are risking back-feeding the Pi 5's RP1 GPIO pins, which are strictly 3.3V tolerant.
3. Verify wire seating. Breadboard contacts wear out; try moving the jumper wires one row down.
Extending and Simplifying the Build
Once the baseline telemetry is flowing, you will likely need to adapt the project to your specific network constraints or hardware limitations.
How to Extend (Add MQTT TLS Encryption)
If your MQTT broker is exposed to the public internet (e.g., hosted on AWS or a VPS), sending telemetry in plaintext is a security risk. Extend the script by adding TLS certificates before the client.connect() call:
# Add to environment setup: pip install certifi
import certifi
client.tls_set(
ca_certs=certifi.where(),
cert_reqs=mqtt.ssl.CERT_REQUIRED,
tls_version=mqtt.ssl.PROTOCOL_TLSv1_2
)
client.username_pw_set("your_username", "your_password")
# Change MQTT_PORT to 8883
How to Simplify (Drop MQTT for Local CSV Logging)
If you are deploying this in an off-grid cabin or a Faraday cage without a network, strip out the paho-mqtt dependency entirely. Replace the MQTT publish block with standard Python file I/O to append to a local CSV. This reduces RAM usage by roughly 12MB and eliminates network timeout exceptions.
import csv
from datetime import datetime
# Inside the while loop, replace client.publish with:
with open("telemetry_log.csv", "a", newline="") as f:
writer = csv.writer(f)
writer.writerow([
datetime.now().isoformat(),
f"{data.temperature:.2f}",
f"{data.humidity:.2f}",
f"{data.pressure:.2f}"
])
By anchoring your Raspberry Pi projects in Python 3, utilizing virtual environments to respect modern OS boundaries, and understanding the physical layer of the I2C bus, you eliminate the most common points of failure before you even write your second script.






