Working With SQLite From an Application

SQLite

Chapter 8 · Working With SQLite From an Application

sqlite1-1 and sqlite1-2 described the "no connection ceremony" workflow. This chapter shows it in real, working application code, in two languages, so the simplicity claim is demonstrated rather than just asserted.

Python's Built-in sqlite3 Module

SQLite support is genuinely built into Python's own standard library — no pip install required at all. This is a real, distinctive fact worth naming: neither MySQL nor Postgres connectivity ships in Python's own standard library; both require a third-party package (mysql-connector-python, psycopg2) just to connect.

import sqlite3

with sqlite3.connect("notes.db") as conn:
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")

    # Parameterized — never string-concatenate user input into SQL
    cursor.execute("INSERT INTO notes (body) VALUES (?)", ("First real note",))
    conn.commit()

    cursor.execute("SELECT * FROM notes")
    for row in cursor.fetchall():
        print(row)

The parameterized execute(sql, (param,)) form is deliberate — a direct callback to sqli1's own parameterization material and postgres1-8's own dynamic-SQL-injection warning. That lesson applies here unchanged, in a completely different language and context. The with sqlite3.connect(...) context-manager form is the idiomatic Python way to guarantee the connection is properly committed and closed.

Node.js's better-sqlite3

better-sqlite3 is a real, popular SQLite binding for Node — and it's deliberately, notably synchronous, in contrast to Node's usual async-everything idioms. The library's own documented reasoning: SQLite operations are so fast that the overhead of async/await machinery genuinely isn't worth it for typical use — a real design decision, not an oversight, and a direct, concrete instance of sqlite1-1's own "near-instant, no handshake" claim.

const Database = require('better-sqlite3');
const db = new Database('notes.db');

db.exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)');

// Parameterized here too
db.prepare('INSERT INTO notes (body) VALUES (?)').run('First real note');

const notes = db.prepare('SELECT * FROM notes').all();
console.log(notes);

Side-by-Side — The Simplicity Gap

MySQL/Postgres connection configSQLite connection config
Host, portA single file path string
Username, password
Database name
Connection pool size
SSL/TLS configuration
A separate driver package to install

Closing Connections & Resource Management

Even though there's no network connection to manage, a SQLite connection object still wraps real operating-system file handles and should still be explicitly closed — via a context manager, a try/finally, or an explicit close() call — when done. It's easy to assume this doesn't matter "since it's just a file," but proper cleanup still matters, especially in the long-running server process context sqlite1-6 covered.

SQL injection is exactly as real here as anywhere else on this site
Even without a username, password, or network exposure to worry about, SQL injection risk is exactly as real inside SQLite application code as anywhere else — sqli1's and postgres1-8's own material applies unchanged. String-concatenating input directly into a SQL string is just as exploitable in a SQLite-backed CLI tool or desktop app as in a networked web application, especially if that "local" input actually originates from an untrusted source — a file the application opens, a value a user pastes in, or data synced in from elsewhere. The absence of a network-facing attack surface doesn't mean the absence of the underlying vulnerability class.
sqlite1-1's own roadmap, delivered
This resolves the "real embedding code" item from sqlite1-1's roadmap. sqlite1-9 covers genuine limitations honestly, before the capstone applies all of this together.

Hands-On Exercises

Exercise 1

Explain why Python's built-in sqlite3 module being part of the standard library (unlike MySQL/Postgres connectivity) is itself a small but real reflection of this course's own throughline.

📄 View solution
Exercise 2

Explain why better-sqlite3's deliberately synchronous design is a genuine, documented choice rather than an oversight, tying your answer back to sqlite1-1's own "near-instant" framing.

📄 View solution
Exercise 3

Using this chapter's own warn-box, explain why SQL injection remains exactly as real a risk in a SQLite-backed application as in a networked MySQL/Postgres application, even without a network-facing attack surface — give a concrete example of an "untrusted local input" source.

📄 View solution

Chapter 8 Quick Reference

  • Python's sqlite3 is built into the standard library — no separate driver install, unlike MySQL/Postgres connectivity
  • Always parameterize (execute(sql, (param,)) / db.prepare(sql).run(param)) — sqli1's/postgres1-8's own lesson applies unchanged
  • better-sqlite3 is deliberately synchronous — SQLite operations are fast enough that async overhead isn't worth it, per the library's own documented reasoning
  • Connection setup: MySQL/Postgres need host/port/user/password/pool/SSL config; SQLite needs a file path string
  • Connections still wrap real file handles and should still be explicitly closed/managed
  • SQL injection risk is unchanged by the absence of a network-facing surface — untrusted local input (files, user-pasted values, synced data) is still a real vector
  • Next chapter: Limitations & Gotchas