Selecting the right Python editor for Raspberry Pi embedded projects dictates how fast you can wire, code, and debug hardware. If you are writing a quick 20-line GPIO blink script, a heavyweight IDE will bog down your Pi's CPU. If you are building a multi-threaded MQTT sensor node, a basic text editor will leave you blind during runtime errors. This guide breaks down the two dominant choices—Thonny and VS Code Remote—using a real-world I2C environmental monitor build to demonstrate setup, execution, and hardware debugging.
The Verdict: Thonny vs VS Code for Embedded Python
The direct answer depends on your hardware setup and project scale. Use Thonny if you are coding directly on the Raspberry Pi (headless or with a monitor) and need instant access to the REPL and GPIO pinouts without configuration. Use VS Code with the Remote-SSH extension if you are writing complex, multi-file applications on a primary PC and deploying to the Pi over your local network.
| Criteria | Thonny (On-Device) | VS Code (Remote-SSH) |
|---|---|---|
| Setup Complexity | Zero (Pre-installed on Pi OS) | Medium (Requires SSH keys & extensions) |
| Hardware Resource Usage | Moderate (~150MB RAM) | Minimal on Pi (Server runs on host PC) |
| GPIO Autocomplete | Basic (Requires manual stubs) | Excellent (Via Pylance & Blinka stubs) |
| Debugging Breakpoints | Visual, step-through, variable watcher | Visual, remote attach, conditional breakpoints |
| Best For | Students, quick sensor tests, Pi 4/5 | Production IoT nodes, Pi Zero, multi-file apps |
Project Build: I2C Environmental Monitor (Parts & Wiring)
To test our Python editor for Raspberry Pi workflow, we will build an I2C environmental monitor. This project reads temperature, humidity, and pressure from a BME280 sensor and displays it on an SSD1306 OLED. This specific combination is notorious for I2C address conflicts and clock-stretching bugs, making it the perfect debugging exercise.
Parts List & Pricing (2026 Estimates)
- Microcontroller: Raspberry Pi 5 (8GB variant) - ~$80.00
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) - ~$19.95
- Display: Adafruit Monochrome 1.3" 128x64 OLED (Product ID: 938) - ~$19.95
- Wiring: 20x female-to-female jumper wires, half-size breadboard - ~$8.00
- OS: Raspberry Pi OS (Bookworm, 64-bit, Desktop)
Pin Mapping Table
The Raspberry Pi 5 retains the standard 40-pin header layout. We are using the primary I2C1 bus. Never connect 5V logic to the Pi's I2C pins; the BCM2712 SoC I2C peripheral is strictly 3.3V tolerant.
| Pi 5 GPIO Pin | Physical Pin # | Function | BME280 Pin | OLED Pin |
|---|---|---|---|---|
| 3V3 Power | 1 | VCC | VIN | VIN |
| Ground | 6 | GND | GND | GND |
| GPIO 2 (SDA1) | 3 | I2C Data | SDA | SDA |
| GPIO 3 (SCL1) | 5 | I2C Clock | SCL | SCL |
Writing and Debugging the Python Code
This code targets the Raspberry Pi 5 8GB running Bookworm 64-bit. It utilizes the adafruit-blinka compatibility layer, which maps CircuitPython hardware APIs to the Pi's Linux sysfs and I2C subsystems.
Prerequisites: Before opening your Python editor, install the required libraries via the Pi's terminal:
sudo apt update
sudo apt install python3-pip i2c-tools
pip3 install --break-system-packages adafruit-circuitpython-bme280 adafruit-circuitpython-ssd1306 pillow
Complete Python Script with Error Handling
Copy this into Thonny or your VS Code remote workspace. Notice the explicit try/except blocks. Hardware initialization fails frequently on the bench; catching these errors prevents silent crashes and gives you actionable terminal output.
import time
import board
import busio
import adafruit_bme280
import adafruit_ssd1306
from PIL import Image, ImageDraw, ImageFont
# Pin definitions: board.SCL maps to GPIO 3, board.SDA maps to GPIO 2
# These are hardcoded by the Blinka library for the Raspberry Pi 5 I2C1 bus.
def initialize_hardware():
"""Initialize I2C bus and sensors with explicit error handling."""
try:
# Set I2C frequency to 100kHz to avoid clock-stretching timeouts on Pi 5
i2c = busio.I2C(board.SCL, board.SDA, frequency=100000)
# BME280 default address is 0x77, but Adafruit breakouts often use 0x76
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
bme280.sea_level_pressure = 1013.25
# SSD1306 128x64 OLED default address is 0x3C
oled = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)
return bme280, oled
except ValueError as e:
print(f"[FATAL] I2C Hardware Initialization Failed: {e}")
print("Check 'i2cdetect -y 1' to verify device addresses.")
exit(1)
except RuntimeError as e:
print(f"[FATAL] Sensor Communication Error: {e}")
exit(1)
def main():
bme280, oled = initialize_hardware()
# Clear the OLED display on startup
oled.fill(0)
oled.show()
# Create a blank image for drawing
image = Image.new('1', (oled.width, oled.height))
draw = ImageDraw.Draw(image)
# Load default font (Pillow built-in)
font = ImageFont.load_default()
print("System Online. Streaming sensor data...")
try:
while True:
temp_c = bme280.temperature
humidity = bme280.relative_humidity
pressure = bme280.pressure
# Draw text on the image buffer
draw.rectangle((0, 0, oled.width, oled.height), outline=0, fill=0)
draw.text((0, 0), f"Temp: {temp_c:.1f} C", font=font, fill=255)
draw.text((0, 20), f"Hum: {humidity:.1f} %", font=font, fill=255)
draw.text((0, 40), f"Pres: {pressure:.0f} hPa", font=font, fill=255)
# Push buffer to hardware
oled.image(image)
oled.show()
# Print to console for Thonny/VS Code debug watcher
print(f"T: {temp_c:.1f}C | H: {humidity:.1f}% | P: {pressure:.0f}hPa")
time.sleep(2.0)
except KeyboardInterrupt:
print("\nScript terminated by user.")
oled.fill(0)
oled.show()
if __name__ == "__main__":
main()
Troubleshooting: "OSError: [Errno 121] Remote I/O error"
When working with I2C on the Raspberry Pi, you will inevitably encounter the following exact error string in your editor's console:
OSError: [Errno 121] Remote I/O error
This is not a Python error; it is a Linux kernel error. The BCM2712 I2C controller sent a clock pulse and an address byte, but the sensor failed to pull the SDA line low to send an ACKnowledge (ACK) bit. The kernel aborts the transaction and passes Errno 121 up to the Python smbus2 or busio layer.
The First Three Things to Check
- Verify the I2C Address Map: Open your Pi terminal and run
i2cdetect -y 1. If the BME280 shows up at0x77instead of0x76, your Python script will throw Errno 121 because it is pinging the wrong address. Update theaddress=0x76parameter in the code to match the grid output. - Check VCC Voltage with a Multimeter: Measure the voltage between the sensor's VCC and GND pins. It must read exactly 3.3V. If you accidentally wired it to the Pi's 5V pin (Physical Pin 2), you may have already burned out the sensor's internal I2C pull-ups, resulting in a permanent Errno 121.
- Inspect SDA/SCL Continuity: Breadboard contacts wear out. Use your multimeter in continuity mode to beep-test the jumper wire from Pi GPIO 2 to the sensor SDA pin. A loose Dupont connector is the cause of this error on the bench at least 50% of the time.
frequency=100000 (100kHz) instead of the default 400kHz to give the sensor more time to process data and release the clock line.
Extending and Simplifying the Build
Once your Python editor for Raspberry Pi workflow is stable and the OLED is displaying data, you can adapt the project to your specific needs.
How to Simplify
If you do not have an OLED display, you can strip out the adafruit_ssd1306 and PIL imports. Rely entirely on the print() statements and use Thonny's built-in "Plotter" feature (View -> Plotter) to graph the temp_c and humidity variables in real-time. This reduces the script to under 30 lines and eliminates the heavy Pillow dependency, which is ideal for headless Pi Zero deployments.
How to Extend
To turn this into a production IoT node, integrate the paho-mqtt library. Wrap the sensor reading loop in a function that publishes a JSON payload to a local Mosquitto broker. You can then use VS Code's Remote-SSH to manage the script as a systemd service, ensuring it restarts automatically if the Pi loses power or the I2C bus locks up. For physical expansion, add a capacitive soil moisture sensor via the Pi's SPI bus (GPIO 10/11/9) to monitor houseplants alongside the ambient room air.
Frequently Asked Questions
What is the best default Python editor for Raspberry Pi beginners?
Thonny is the undisputed best default editor for beginners. It comes pre-installed on Raspberry Pi OS Desktop, requires zero configuration to access GPIO, and features a built-in variable explorer and serial plotter. Its step-through debugger allows you to pause execution and inspect the exact state of your I2C bus objects before a hardware fault occurs.
Can I use VS Code as a remote Python editor for Raspberry Pi?
Yes, and it is the recommended workflow for advanced users. By installing the "Remote - SSH" extension in VS Code on your main Windows/Mac/Linux machine, you can connect to your Pi's IP address. VS Code installs a lightweight server on the Pi, giving you full IntelliSense, Pylance type-checking, and integrated terminal access without taxing the Pi's CPU or RAM with a heavy GUI.
Why does my Python editor Raspberry Pi GPIO script fail on boot?
If your script runs perfectly in Thonny but fails when executed via crontab or rc.local on boot, it is almost always a path or permissions issue. The adafruit-blinka library requires access to /dev/i2c-1, which requires the user to be in the i2c and gpio groups. Furthermore, cron runs with a limited environment PATH. Always use absolute paths in your boot scripts (e.g., /usr/bin/python3 /home/pi/project/main.py).
How do I run my Python editor Raspberry Pi script in the background?
Do not rely on appending an ampersand (&) or using nohup for long-term embedded projects. The professional standard is to create a systemd service file. Create a file at /etc/systemd/system/env-monitor.service, define your ExecStart path, and enable it with sudo systemctl enable env-monitor. This ensures your script runs under a managed daemon, captures stdout to the system journal for debugging, and automatically restarts if the code throws an unhandled exception.






