Topic Web and software engineering

IoT With Python: From Device Data to a Live Dashboard

The short answer

IoT with Python usually means MicroPython on the device, full Python on a gateway such as a Raspberry Pi, and Python on the server that stores and serves the data. Readings travel over MQTT, land in a time-series store, and appear on a web dashboard. Python is the wrong choice for hard real-time control and very low-power devices.

Written by

Siddharth Kajaria, Founder and CEO

Published

Updated

Reading time

10 min read

Seedling trays in a greenhouse with small sensors on stakes linked by dotted lines to a wall tablet showing one line.
In this post10 min read
  1. Is Python good for IoT?
  2. The architecture: from sensor to dashboard
  3. Step 1: read a sensor and publish it with MicroPython
  4. Step 2: run an MQTT broker
  5. Step 3: store the readings with Python
  6. Step 4: serve the data to a live dashboard
  7. Security: what to get right from the first device
  8. When is Python the wrong layer?
  9. From prototype to product
  10. How we’d approach an IoT build
  11. The short version

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:

  1. Sensors and devices. A microcontroller such as an ESP32 or a Raspberry Pi Pico W reads a sensor and runs MicroPython.
  2. 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.
  3. A message broker. An MQTT broker such as Mosquitto receives readings from every device and passes them to whoever subscribes.
  4. An ingestion service. A Python program subscribes to the broker, checks each reading and stores it.
  5. A data store. SQLite for a prototype, a time-series database such as TimescaleDB or InfluxDB for a product.
  6. 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.

Planning something like this?

Tell us what you are building, and we will talk it through with you.

Book a call

02 FAQ

Questions, answered

Which microcontroller boards run MicroPython?
MicroPython runs on many boards, including the ESP32 and ESP8266, the Raspberry Pi Pico and Pico W, and STM32 boards such as the original Pyboard. For a connected sensor, pick a board with built-in Wi-Fi, such as the ESP32 or the Pico W, so you don't need a separate network module.
Should you use MicroPython or full Python on the device?
Use MicroPython on microcontrollers: small, cheap, low-power boards with kilobytes of memory and no operating system. Use full Python on a single-board computer such as a Raspberry Pi, which runs Linux and can use any library from PyPI. Many systems use both: MicroPython sensors reporting to a Raspberry Pi gateway.
How do IoT devices send data to Python?
Most often over MQTT, a lightweight publish and subscribe protocol. Devices publish readings to topics on a broker such as Mosquitto, and a Python service subscribes with the paho-mqtt library and stores each message. HTTP works for devices that report rarely, and CoAP suits very constrained networks.
Which database should store IoT sensor data?
For a prototype, SQLite is enough. For a product, use a time-series database such as TimescaleDB (built on PostgreSQL) or InfluxDB, which store timestamped readings efficiently, summarise them by minute or hour, and drop old raw data on a schedule you set.