An IoT system is a chain: a sensor takes a reading, a device sends it, something receives and stores it, and someone sees it on a screen. Python can do every link in that chain except the tightest, lowest-power one, which is why it is the most common way developers prototype connected devices and a sensible choice for much of a production system too.
This guide builds that chain end to end with Python: a temperature and humidity sensor on a microcontroller, readings sent over MQTT, a Python service that stores them, and an API a live dashboard can read. Along the way it covers security, when Python is the wrong layer, and what changes when a prototype becomes a product.
Is Python good for IoT?
Python is good for IoT everywhere except the most constrained devices: it runs on microcontrollers through MicroPython, on gateways as full Python, and on the servers that store, analyse and display the data. Its weak spot is hard real-time control and devices that must last years on a small battery.
Why teams choose it:
- One language across the system. The same team can write the device code, the ingestion service, the data processing and the API.
- Fast iteration on the device. MicroPython has an interactive prompt, so you can read a sensor and see the result in seconds, without a compile and flash cycle.
- Strong data tooling. Once readings are stored, pandas, NumPy and scikit-learn handle cleaning, analysis and anomaly detection.
- Mature libraries for the plumbing. paho-mqtt for messaging, FastAPI or Django for the API, and drivers for most common sensors.
The architecture: from sensor to dashboard
A typical small IoT system with Python has six parts:
- Sensors and devices. A microcontroller such as an ESP32 or a Raspberry Pi Pico W reads a sensor and runs MicroPython.
- A gateway (optional). A Raspberry Pi running full Python collects data from devices that can’t reach the network themselves, such as Bluetooth or wired Modbus sensors.
- A message broker. An MQTT broker such as Mosquitto receives readings from every device and passes them to whoever subscribes.
- An ingestion service. A Python program subscribes to the broker, checks each reading and stores it.
- A data store. SQLite for a prototype, a time-series database such as TimescaleDB or InfluxDB for a product.
- An API and a dashboard. A small Python API serves recent readings to a web dashboard or a mobile app.
MQTT sits at the centre because it was built for this job: devices publish small messages to named topics (sensors/room-1/reading), the broker forwards them, and subscribers never need to know where a device is or when it is online.
Step 1: read a sensor and publish it with MicroPython
This runs on an ESP32 with MicroPython installed and a DHT22 temperature and humidity sensor wired to pin 4. Install the MQTT client on the board once with mpremote mip install umqtt.simple, then save this as main.py:
# main.py on an ESP32 running MicroPython
import json
import time
import dht
import machine
import network
from umqtt.simple import MQTTClient
WIFI_SSID = "your-network"
WIFI_PASSWORD = "your-password"
BROKER = "192.168.1.20" # the machine running Mosquitto
DEVICE_ID = "room-1"
TOPIC = b"sensors/" + DEVICE_ID.encode() + b"/reading"
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
while not wlan.isconnected():
time.sleep(1)
sensor = dht.DHT22(machine.Pin(4))
client = MQTTClient(DEVICE_ID, BROKER, user=DEVICE_ID, password="this-device-secret")
client.connect()
while True:
try:
sensor.measure()
payload = json.dumps({"t": sensor.temperature(), "h": sensor.humidity()})
client.publish(TOPIC, payload)
except OSError:
pass # a failed sensor read: skip it and try again next cycle
time.sleep(60)
The device sends one small JSON message a minute. It doesn’t add a timestamp: a microcontroller’s clock is unreliable until it is synced, so the ingestion service stamps each reading when it arrives. A production version would also reconnect when Wi-Fi or the broker drops, and sleep between readings to save power.
Using a Raspberry Pi instead
On a Raspberry Pi you have full Python and Linux, so you can use the Adafruit driver (pip install adafruit-circuitpython-dht) and paho-mqtt directly:
import adafruit_dht
import board
sensor = adafruit_dht.DHT22(board.D4)
print(sensor.temperature, sensor.humidity)
A Pi suits a gateway that collects from many sensors, or a device that needs a camera, local storage or heavier processing. A microcontroller suits a device that must be cheap, small and low-power.
Step 2: run an MQTT broker
Mosquitto is a widely used open-source MQTT broker that runs on a Raspberry Pi, a laptop or a small cloud server. Turn off anonymous access from the start, and give every device its own username and password:
# /etc/mosquitto/conf.d/iot.conf
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
Create the accounts with mosquitto_passwd -c /etc/mosquitto/passwd ingest (drop -c for every account after the first), then limit what each one may do:
# /etc/mosquitto/acl
user ingest
topic read sensors/#
pattern write sensors/%u/reading
The pattern line means a device can publish only to its own topic: the device logged in as room-1 can write to sensors/room-1/reading and nowhere else. If one device is compromised, it can’t impersonate the others. On any network you don’t fully control, add a TLS listener on port 8883 so readings and passwords are encrypted in transit.
Step 3: store the readings with Python
The storage module is plain Python and SQLite, so you can run and test it on any machine. It checks each reading before saving it, because sensors do produce impossible values:
# store.py
import json
import sqlite3
DB_PATH = "readings.db"
def connect() -> sqlite3.Connection:
db = sqlite3.connect(DB_PATH)
db.execute(
"CREATE TABLE IF NOT EXISTS readings ("
" device_id TEXT, ts INTEGER, temperature REAL, humidity REAL)"
)
return db
def save_reading(topic: str, payload: bytes, ts: int) -> None:
device_id = topic.split("/")[1] # sensors/<device_id>/reading
data = json.loads(payload)
t, h = float(data["t"]), float(data["h"])
if not (-40 <= t <= 80 and 0 <= h <= 100):
return # outside what a DHT22 can measure: a bad read
with connect() as db: # commits on success
db.execute("INSERT INTO readings VALUES (?, ?, ?, ?)", (device_id, ts, t, h))
def latest(device_id: str, limit: int = 100) -> list:
with connect() as db:
rows = db.execute(
"SELECT ts, temperature, humidity FROM readings"
" WHERE device_id = ? ORDER BY ts DESC LIMIT ?",
(device_id, limit),
).fetchall()
return [{"ts": ts, "t": t, "h": h} for ts, t, h in rows]
Then the ingestion service subscribes to every device’s topic with paho-mqtt (pip install paho-mqtt; this uses the version 2 callback style):
# ingest.py
import time
import paho.mqtt.client as mqtt
from store import save_reading
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe("sensors/+/reading") # re-subscribes after every reconnect
def on_message(client, userdata, msg):
try:
save_reading(msg.topic, msg.payload, int(time.time()))
except (ValueError, KeyError, TypeError):
pass # a malformed message: log it in a real system
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.username_pw_set("ingest", "the-ingest-password")
client.on_connect = on_connect
client.on_message = on_message
client.connect("localhost", 1883)
client.loop_forever()
The + in the topic is a single-level wildcard, so one subscription covers every device. Subscribing inside on_connect matters: if the broker restarts, the client reconnects and subscribes again on its own.
Step 4: serve the data to a live dashboard
A small API turns the stored readings into something a dashboard or an app can fetch. With FastAPI (pip install fastapi uvicorn):
# api.py
from fastapi import FastAPI
from store import latest
app = FastAPI()
@app.get("/api/devices/{device_id}/readings")
def readings(device_id: str, limit: int = 100):
return latest(device_id, limit)
Run it with uvicorn api:app, and /api/devices/room-1/readings returns the latest readings as JSON. From there you have three common options for the screen itself:
- Grafana, which connects to TimescaleDB or InfluxDB directly and suits internal monitoring.
- A notebook or a script with pandas for analysis rather than monitoring. Our guide to analyse the data with pandas covers that side.
- A custom web dashboard or app, when the screen is for customers, needs your branding or combines device data with other business data. That is where dashboards for live data built for the purpose earn their cost.
For a dashboard that updates without refreshing, add a WebSocket or server-sent events endpoint that pushes each new reading as it arrives.
Security: what to get right from the first device
Connected devices are a common way into a network, so security belongs in the prototype, not the launch checklist:
- A unique identity for every device. Its own credentials or, better, its own client certificate. Never one shared password across the fleet.
- No default passwords. Each device gets its credentials when it is provisioned, and you can revoke one device without touching the rest.
- Encryption in transit. MQTT over TLS on port 8883 whenever traffic leaves a network you control.
- Least privilege on the broker. Access rules so each device can publish only to its own topics and read only its own commands.
- Signed over-the-air updates. You will need to fix bugs on devices already in the field. Updates must be signed, so a device installs only firmware you produced, and it must be able to roll back a failed update.
- Validate everything that arrives. Treat every message as untrusted input: check its shape and its range, as
save_reading()does above.
When is Python the wrong layer?
Python is the wrong layer when timing must be guaranteed to the microsecond, when a device must run for years on a coin cell, or when the hardware has too little memory for an interpreter. In those cases the device firmware is written in C, C++ or Rust, and Python stays on the gateway and the server.
Signs you have reached that point:
- Hard real-time control, such as motor control or safety cut-offs, where a garbage-collection pause is unacceptable.
- Extreme power budgets. MicroPython can sleep between readings, but compiled firmware draws less power for the same work.
- Very small chips with too little RAM or flash for the MicroPython runtime.
- Large fleets with tight hardware costs, where a cheaper chip saves more than the faster development is worth.
A common pattern is to prototype everything in MicroPython, then rewrite only the device firmware in C or Rust once the design is proven, keeping the Python back end unchanged.
From prototype to product
A working prototype on a desk is the smaller part of the job. The larger part is what a fleet of devices in the field needs:
- Provisioning. How each device gets its identity, credentials and Wi-Fi settings at the factory or on first setup, without someone typing passwords into code.
- Fleet monitoring. Which devices are online, their battery levels and firmware versions, and an alert when one goes quiet.
- Over-the-air updates. Staged roll-outs to a few devices first, with automatic rollback.
- Data at scale. A time-series database, retention rules (raw readings for weeks, hourly summaries for years) and backups.
- The app people use. A dashboard, a mobile app or both, with accounts, alerts and permissions. Our wearable and connected-device apps page covers the app side.
- Field realities. Weak Wi-Fi, power cuts, heat and dust. Test hardware where it will actually be used.
For an overview of how the whole build fits together, from hardware to app store, see how IoT app development works.
How we’d approach an IoT build
We’d start from the screen and work backwards: who needs to see what, how fresh it must be, and which decision it supports. That answer sets how often devices report, what the back end stores and what the dashboard shows. Then we’d prove the riskiest link first, usually the device in its real environment, before building the dashboard around it. The back end, ingestion and API are where our Python development work fits.
For Homa’s IoT atta chakki, Tailwebs built the full IoT ecosystem: the customer mobile app, a Python (Django) backend and admin, device connectivity over MQTT, and over-the-air firmware updates.
The short version
- MicroPython on the device, full Python on a gateway, and Python on the server is a complete, practical IoT stack.
- MQTT with a broker such as Mosquitto carries the readings, with a separate login and topic for each device.
- A Python service validates and stores each reading, and a small API feeds the dashboard.
- Build security in from the first device: unique identities, TLS, broker access rules and signed updates.
- Move device firmware to C, C++ or Rust only when real-time, power or cost limits demand it.
