Chapter 15: Database Fundamentals with SQLite

Introduction to SQL and SQLite

Up to now, most of your API projects have probably been stateless: run the script, fetch some data, print the result, then exit. The moment the Python process stops, everything disappears with it. That works for experiments and one-run utilities, but real applications often need state: a way to store data permanently so it can be queried, updated, and reused across runs.

Terminal window showing a Python weather script run twice. The first run prints the current weather, while the second run starts fresh and finds no saved weather history.
Each run starts fresh unless the program saves data somewhere outside its own memory.

Persistent storage solves that problem, and SQLite is the fastest path to it, with no server to install, no configuration files, no ports, and no separate database service to manage.

SQLite

SQLite is embedded. It stores data in a single file on the host device, and the database engine runs inside the application process. Python includes built-in support for SQLite through the standard-library sqlite3 module. To use it, you import sqlite3 and connect to a database file.

A laptop displaying a single file labelled weather.db with the SQLite logo, illustrating that a SQLite database is one file stored locally on the device.
SQLite is a relational database: multiple tables can live inside that file and link to each other through shared keys.

Because the database is embedded, your application can read and write local data directly, without sending queries to a separate database server. That makes SQLite simple to set up and very fast for local workloads, especially small applications, prototypes, and single-user tools.

SQLite is often misunderstood as a tool meant only for small, temporary scripts. Think of it instead as the database you reach for when an application needs its own local store on the same device. That is why SQLite shows up in mobile apps, desktop software, developer tools, browsers, and embedded devices: not because those systems are unserious, but because local storage is exactly the right shape for the problem.

When you create a new SQLite database, you specify a filename, and that file becomes the container for your data. In this chapter, that file is weather.db. Inside it, you can define one or more tables to organise data into rows and columns.

Each table has a schema: the column names, data types, and rules that define what each row should contain. This chapter's main project starts with a single weather_readings table. The tree below also shows two small teaching databases introduced later: one for cache-style upserts and one for foreign keys and JOINs.

weather.db
└── weather_readings

weather_demo.db
├── locations
└── weather_readings

weather_cache_demo.db
└── weather_cache

weather.db is this chapter's main project with one table weather_readings. weather_demo.db appears on the foreign keys and JOINs page, and weather_cache_demo.db on the inserting rows page, both for teaching purposes.

A useful mental model

Think of SQLite as the storage engine and SQL as the language you use to talk to it. Python orchestrates the application, SQLite stores the data, and SQL is the instruction language between them.

SQL is a language for working with data

SQL (Structured Query Language) is the standard language used to work with relational databases such as SQLite, PostgreSQL, and MySQL. The details vary depending on which database system you're using, but the core ideas remain the same.

SQL lets you define tables, insert data, search for rows, update records, delete records, and combine information across multiple tables. Python can control the overall program, while SQL focuses on the database: storing data, organising it, and retrieving exactly the pieces you need.

Here's a preview of the kind of statement you'll write yourself on the schema basics page:

SQL language
CREATE TABLE IF NOT EXISTS weather_readings (
    id           INTEGER PRIMARY KEY,
    location     TEXT NOT NULL,
    temperature  REAL,
    conditions   TEXT,
    humidity     INTEGER,
    wind_speed   REAL,
    recorded_at  DATETIME DEFAULT CURRENT_TIMESTAMP
);

You can run SQL statements like the one above directly from a database CLI in your terminal, or you can embed them as strings inside your Python code and execute them with the sqlite3 module.

Creating a database and a table

In the script below, the SQL statement sits inside a multiline Python string passed to conn.execute(): two languages in one call, Python on the outside and SQL inside the string.

The Python on the outside opens a connection to weather.db with sqlite3.connect(), creating the file on disk if it doesn't already exist. The with block is Python's standard idiom for using a connection; the mechanics get a fuller treatment later in the chapter.

Python: table creation
import sqlite3

with sqlite3.connect("weather.db") as conn:
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS weather_readings (
            id           INTEGER PRIMARY KEY,
            location     TEXT NOT NULL,
            temperature  REAL,
            conditions   TEXT,
            humidity     INTEGER,
            wind_speed   REAL,
            recorded_at  DATETIME DEFAULT CURRENT_TIMESTAMP
        );
        """
    )

Each call to conn.execute() ships a SQL string to the engine and hands the result back to Python. The rest of the chapter builds up the patterns around that single call: parameter binding, transaction control, and row factories.

What you'll learn

This chapter turns a "one-run" API script into a small database-backed application. The project is a weather cache: Python fetches current weather from an API, SQLite stores each reading on disk, and SQL lets your code reuse fresh data, inspect history, and calculate simple trends.

By the end, you'll know how to
  • Create a database file your scripts can read from and write to
  • Save API responses into it safely, without quoting bugs or lost data
  • Read back exactly the rows you need: newest first, filtered by city, or summarised across a week
  • Decide when to keep history and when to overwrite an older entry
  • Keep queries fast as the stored data grows
  • Connect related tables with foreign keys and JOINs
  • Use Python's built-in sqlite3 module the way real applications do
  • Know when SQLite is the right tool, and when you'd reach for PostgreSQL

The chapter starts with the smallest useful idea: a script forgets everything when it exits. From there, each page adds one database habit until the final WeatherCache class can decide whether to return a stored reading or call the API again.