Part
1
  |  
First Principles
  |  
Chapter
2

Python for the Pi

You don't need to master all of Python before touching hardware — you need the 20% that does 80% of GPIO work, and nothing else.
Reading Time
12
mins
BACK TO RASPBERRY PI MASTERCLASS

Every Python course starts at the beginning and promises to take you to the end. Four hundred lessons later, you understand decorators, metaclasses, async context managers, and generator pipelines. You still haven't blinked an LED. The completionist instinct — learn everything about the language before applying it — is one of the most effective procrastination strategies in engineering. It feels productive. It looks like learning. And it delays the moment you actually build something by weeks or months.

The completionist instinct — learn everything about the language before applying it — is one of the most effective procrastination strategies in engineering.

Here is the uncomfortable truth: I run production Pi systems that monitor sensors, process camera feeds, control relays, and serve API endpoints. The Python in those systems uses variables, functions, conditionals, loops, imports, f-strings, and try/except. That's it. No classes. No decorators. No async/await. No dataclasses. No list comprehensions fancier than a simple filter. The advanced features exist and they're useful — in application development. In hardware work, they're overhead. The sensor doesn't care how elegant your code is. It cares whether you read the right register and handled the exception when the I2C bus hiccups.

This chapter is the Python you need for the Pi. If you already know Python, skim it as a refresher and pay attention to the hardware-specific patterns. If you're new to Python, this chapter gets you to "functional with GPIO" in one sitting. Everything else can wait.

The Hardware Pythonista's Subset

Framework · The Hardware Pythonista's Subset · HPS

The 20% of Python that handles 80% of Raspberry Pi work: variables, control flow, functions, modules, f-strings, and try/except. Master these six, and you can build any GPIO project in this book. Everything else is optimization, not prerequisite.

I arrived at this subset by auditing every Pi project I've built in the last three years and tallying which language features actually appeared in production code. The results were lopsided. Variables and basic types appeared in 100% of files. Functions with default arguments appeared in 95%. Try/except blocks appeared in 90% — hardware is unreliable, and you handle that at the call site, not in an abstract error hierarchy. Imports and f-strings appeared in every single file. Classes appeared in less than 10% of files, and every time, a module with functions would have been cleaner.

This isn't an argument against learning advanced Python. It's an argument against blocking on it. Learn the subset, build things, and pick up advanced features when a real project demands them — not when a curriculum says it's time.

Variables, Types, and Operators

Python variables hold values. You don't declare types — the interpreter figures it out:

# Numbers — the foundation of sensor readings
temperature = 23.5        # float
pin_number = 17           # int
is_active = True          # bool

# Strings — for logging, display, and API payloads
sensor_name = "DHT22"
status = "online"

# Arithmetic operators you'll use constantly
celsius = 23.5
fahrenheit = (celsius * 9/5) + 32    # 74.3
readings_count = 0
readings_count += 1                   # increment

# Comparison operators — used in every conditional
if temperature > 30.0:
    print("Too hot")
if is_active and temperature < 0:
    print("Freezing and active — check the sensor")

Three things matter here for hardware work. First, int vs float: GPIO pin numbers are always integers, sensor readings are almost always floats. Mixing them up causes subtle bugs. Second, bool: you'll use booleans constantly to track pin states (HIGH/LOW), sensor status, and system flags. Third, the += operator for incrementing counters — you'll count events, readings, and errors in every project.

Type hints — useful but optional

Python supports type hints like temperature: float = 23.5. I use them in larger projects for clarity, but they're not required and they don't change how the code runs. Don't let type-hint syntax slow you down when you're prototyping. Add hints later when the project stabilizes.

F-Strings and String Formatting

Every Pi project needs formatted output — for logging, for display, for sending data to an API. F-strings are the only formatting method worth learning in modern Python:

sensor = "DHT22"
temp = 22.7
humidity = 45.3
pin = 4

# Basic f-string
print(f"Sensor {sensor} on GPIO {pin}: {temp}°C")
# Output: Sensor DHT22 on GPIO 4: 22.7°C

# Format specifiers for clean output
print(f"Temperature: {temp:.1f}°C | Humidity: {humidity:.0f}%")
# Output: Temperature: 22.7°C | Humidity: 45%

# Expressions inside braces
print(f"Fahrenheit: {(temp * 9/5) + 32:.1f}°F")
# Output: Fahrenheit: 72.9°F

# Timestamps — you'll use this in every logging function
from datetime import datetime
now = datetime.now()
print(f"[{now:%Y-%m-%d %H:%M:%S}] Reading: {temp}°C")
# Output: [2025-01-15 14:23:07] Reading: 22.7°C

The :.1f format specifier (one decimal place) is the one you'll reach for most. Sensor readings at full float precision are unreadable noise. Format them.

Key takeaway

F-strings handle 100% of the string formatting you'll need on the Pi. Learn f-strings, skip .format() and % formatting entirely — they're legacy approaches that still work but add nothing.

Lists, Tuples, and Dictionaries

Three data structures cover every Pi use case:

# Lists — mutable, ordered, your primary data container
readings = [22.5, 22.7, 23.1, 22.9, 23.0]
readings.append(23.2)              # add a reading
latest = readings[-1]              # last element: 23.2
last_five = readings[-5:]          # last 5 elements
average = sum(readings) / len(readings)

# Tuples — immutable, for fixed pairs and return values
rgb_color = (255, 128, 0)          # LED color
sensor_location = ("Lab", "Shelf 3", "Position A")
# Unpack directly
r, g, b = rgb_color

# Dictionaries — key-value pairs for structured sensor data
reading = {
    "sensor": "DHT22",
    "temperature": 22.7,
    "humidity": 45.3,
    "timestamp": "2025-01-15T14:23:07",
    "pin": 4
}
print(f"Temp: {reading['temperature']}°C")

# Nested dictionaries — for multi-sensor setups
sensors = {
    "indoor": {"pin": 4, "type": "DHT22"},
    "outdoor": {"pin": 17, "type": "DS18B20"}
}
indoor_pin = sensors["indoor"]["pin"]   # 4

Lists hold time-series sensor data. Dictionaries hold structured readings you'll serialize to JSON and send over the network. Tuples hold fixed groupings like RGB values or GPIO pin assignments. That's the entire data structure story for Pi work.

Conditionals and Loops

Control flow reads like English in Python, which is one reason it became the dominant language for hardware:

# Conditionals — your decision engine
temperature = 28.5
if temperature > 30:
    print("ALERT: overheating")
    activate_fan = True
elif temperature > 25:
    print("Warm — monitoring")
    activate_fan = False
else:
    print("Normal range")
    activate_fan = False

# For loops — iterate over sensor readings
readings = [22.5, 23.1, 29.8, 22.7, 31.2]
for reading in readings:
    if reading > 30:
        print(f"Spike detected: {reading}°C")

# For loop with enumerate — when you need the index
for i, reading in enumerate(readings):
    print(f"Reading {i + 1}: {reading}°C")

# While loops — the heartbeat of every Pi project
import time

running = True
while running:
    temperature = read_sensor()     # your sensor function
    print(f"Current: {temperature}°C")
    if temperature > 40:
        print("Critical — shutting down")
        running = False
    time.sleep(1)                   # wait 1 second between readings

That while True loop with a time.sleep() inside it is the most common pattern in Pi programming. Nearly every sensor-monitoring script follows this shape: loop forever, read a sensor, act on the reading, sleep, repeat. The sleep interval is your sampling rate. One second for temperature. A hundred milliseconds for motion detection. Ten seconds for a weather station. Match it to the physics of what you're measuring.

Breaking out of loops cleanly

Never use while True without a clean exit path. Always handle KeyboardInterrupt (Ctrl+C) so your script releases GPIO pins when you stop it. An orphaned GPIO pin that's still set to HIGH can damage connected components. Section on try/except below covers this.

Functions with Defaults

Functions turn repeated patterns into reusable tools. For Pi work, default arguments are the feature that matters most:

import time
from datetime import datetime

def read_and_log(pin, sensor_type="DHT22", interval=1.0, log_file=None):
    """Read a sensor and optionally log to a file."""
    value = read_sensor(pin)
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    message = f"[{timestamp}] {sensor_type} on GPIO {pin}: {value}"
    print(message)

    if log_file:
        with open(log_file, "a") as f:
            f.write(message + "\n")

    return value

# Call with just the pin — defaults handle the rest
read_and_log(4)

# Override what you need
read_and_log(17, sensor_type="DS18B20", interval=5.0, log_file="/home/pi/temps.log")

Default arguments let you write one function that serves multiple use cases without passing every parameter every time. I've seen this pattern where a hardware project starts with a dozen copy-pasted sensor-reading blocks that differ by one or two values. A single function with defaults replaces all of them and makes the main loop readable.

A single function with defaults replaces a dozen copy-pasted sensor-reading blocks and makes the main loop readable.

Importing Modules

Modules are Python's mechanism for using other people's code. On the Pi, you'll import from three sources:

# Standard library — ships with Python, always available
import time                    # sleep, timing
import json                    # serialize sensor data
import os                      # file paths, environment variables
from datetime import datetime  # timestamps

# Pi-specific libraries — installed via pip or apt
import RPi.GPIO as GPIO        # direct GPIO control
from gpiozero import LED, Button, MotionSensor   # higher-level GPIO
import board                   # Adafruit's board pin definitions
import adafruit_dht            # Adafruit's DHT sensor library

# Your own modules — organize your project
from sensors import read_temperature, read_humidity
from alerts import send_email_alert

Two conventions to internalize. First, import RPi.GPIO as GPIO — the alias is universal in Pi code, and every example online uses it. Adopt it for readability. Second, from gpiozero import LEDgpiozero is a higher-level library that wraps RPi.GPIO with cleaner abstractions. Use gpiozero when it supports your component; drop to RPi.GPIO when you need lower-level control.

Key takeaway

You'll import from three sources on the Pi: Python's standard library (time, json, os), Pi-specific hardware libraries (RPi.GPIO, gpiozero, adafruit), and your own project modules. That's the entire import story.

File I/O for Logging and Configuration

Every Pi project that runs longer than a demo needs to write data to disk and read configuration from files:

# Writing sensor data — append mode so you don't lose history
def log_reading(filepath, sensor, value):
    from datetime import datetime
    timestamp = datetime.now().isoformat()
    line = f"{timestamp},{sensor},{value}\n"
    with open(filepath, "a") as f:
        f.write(line)

# Usage
log_reading("/home/pi/data/readings.csv", "DHT22", 23.5)

# Reading configuration from a JSON file
import json

def load_config(filepath="/home/pi/config.json"):
    with open(filepath, "r") as f:
        return json.load(f)

# config.json: {"pin": 4, "interval": 5, "alert_threshold": 30.0}
config = load_config()
print(f"Monitoring GPIO {config['pin']} every {config['interval']}s")

# Reading sensor data back for analysis
def read_csv(filepath):
    readings = []
    with open(filepath, "r") as f:
        for line in f:
            parts = line.strip().split(",")
            if len(parts) == 3:
                timestamp, sensor, value = parts
                readings.append({"timestamp": timestamp, "sensor": sensor, "value": float(value)})
    return readings

The pattern is always the same: with open(path, mode) as f:. The with statement guarantees the file closes even if an error occurs — critical on the Pi where unclosed file handles can accumulate and crash long-running scripts. Use "a" (append) for logging, "r" for reading, "w" for overwriting.

SD card writes

The Pi's SD card has a finite number of write cycles. Writing sensor data every second, 24/7, will degrade the card within a year. For high-frequency logging, write to RAM (/tmp/) and flush to the SD card periodically, or use a USB drive for storage. Chapter 3 covers choosing the right SD card.

Try/Except — The Hardware Safety Net

This is the section that matters most for hardware work. Sensors disconnect. I2C buses hang. SPI transactions time out. WiFi drops. Power fluctuates. Every one of these failures throws an exception in Python, and if you don't catch it, your script crashes and leaves GPIO pins in whatever state they were in when it died.

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.OUT)

try:
    while True:
        GPIO.output(17, GPIO.HIGH)
        time.sleep(1)
        GPIO.output(17, GPIO.LOW)
        time.sleep(1)

except KeyboardInterrupt:
    print("\nStopped by user")

except Exception as e:
    print(f"Error: {e}")

finally:
    GPIO.cleanup()
    print("GPIO pins cleaned up")

That try/except/finally pattern is non-negotiable for any script that touches GPIO. The finally block runs whether the script exits normally, gets interrupted by Ctrl+C, or crashes from an unexpected error. GPIO.cleanup() resets all pins to input mode, preventing electrical damage to connected components.

For sensor reading specifically, the pattern wraps the read call and provides a fallback:

def safe_read(sensor, retries=3, delay=2.0):
    """Read a sensor with retries — hardware is unreliable."""
    for attempt in range(retries):
        try:
            value = sensor.read()
            if value is not None:
                return value
            print(f"Attempt {attempt + 1}: got None, retrying...")
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
        time.sleep(delay)

    print(f"All {retries} attempts failed")
    return None

This retry pattern shows up in every production Pi system I run. Hardware errors are usually transient — a noisy I2C bus, a sensor that needs a moment to stabilize, a timing glitch. Retrying with a short delay fixes most of them. The alternative — a crashed script at 3 AM with no readings since the failure — is not acceptable in production.

✕ Without try/except
  • Script crashes on first sensor hiccup
  • GPIO pins left in unknown state
  • No error logging — you discover the failure hours later
  • Must restart manually after every glitch
✓ With try/except + retries
  • Transient errors handled silently
  • GPIO always cleaned up on exit
  • Errors logged with timestamps for debugging
  • Script runs for months without intervention

What You Don't Need (Yet)

I want to be explicit about what you can skip for now, because the internet will tell you otherwise:

  • Classes and OOP — Functions are enough for 90% of Pi projects. When a project grows large enough to benefit from classes, you'll know because your functions start sharing a lot of state. Until then, a module with functions is cleaner and easier to debug.
  • Async/await — Tempting for concurrent sensor reading, but time.sleep() and threading handle most Pi concurrency. Async adds complexity that pays off in web servers, not in GPIO scripts.
  • Decorators — Elegant for web frameworks, unnecessary for hardware. If you need a retry wrapper, write a function that takes a function as an argument. You don't need @retry syntax to accomplish that.
  • List comprehensions — Fine to use if you already know them. Not worth learning before you've built your first project. A for loop with an append does the same thing with more clarity.
  • Virtual environments — Important for Python development in general. On a dedicated Pi running one project, a system-wide pip install is fine. Revisit this when you're running multiple projects on one board.

The sensor doesn't care how elegant your code is. It cares whether you read the right register and handled the exception when the I2C bus hiccuped.

What to Do Monday Morning

Open a Python REPL on your computer and write a sensor-simulation loop

You don't need a Pi yet. Write a while True loop that generates a random temperature between 18.0 and 35.0, prints it with an f-string and a timestamp, checks if it exceeds a threshold, and sleeps for one second. This is the skeleton of every Pi sensor script.

Write a function with defaults that logs readings to a CSV file

Take the loop from Step 1 and extract the logging into a function with parameters for the file path, sensor name, and value. Use default arguments for the file path. Open the resulting CSV in a spreadsheet app and verify the data is clean.

Add try/except/finally to your loop

Wrap the main loop in try/except. Catch KeyboardInterrupt separately and print a clean exit message. Add a finally block that prints "Cleanup complete." Run the script and press Ctrl+C. Verify the finally block executes. This muscle memory saves you from GPIO damage on real hardware.

Read one Python module's source code

Pick any small Python library — gpiozero's LED class is ideal — and read the source. Find the functions, the imports, the try/except blocks. Notice what advanced features it uses and which it doesn't. This builds reading fluency faster than any tutorial.

The trap isn't that Python is hard. The trap is that Python is deep, and depth masquerades as prerequisite. You don't need the bottom of the pool to swim across it. The Hardware Pythonista's Subset — variables, functions, loops, imports, f-strings, try/except — gets you across. Everything else is a luxury you add when a real project demands it, not before.