Python SQL CRUD with Python

Perform all four CRUD operations from Python with parameterized SQL, check how many rows changed, and group related changes in transactions.

Lesson 68 of 107Advanced13 min read

CRUD stands for the four basic database operations: Create (INSERT), Read (SELECT), Update (UPDATE) and Delete (DELETE). Almost every application that stores data, from a to-do list to an online shop, is built from these four. This lesson performs each one from Python using the built-in sqlite3 module, so everything runs without installing a database server:

Python
import sqlite3
db = sqlite3.connect("repairs.db")
db.execute("INSERT INTO repairs (item, status) VALUES (?, ?)", ("Lamp", "waiting"))
db.commit()

The same patterns apply to MySQL, PostgreSQL and other databases; only the driver and placeholder style change. The examples track items at a community repair café. Basic SQL knowledge helps; the SQL course teaches it from scratch.

Create and read

Creating the table is a one-off schema step, separate from adding data:

Python
import sqlite3
db = sqlite3.connect("repairs.db")
db.execute("""
    CREATE TABLE IF NOT EXISTS repairs (
        id INTEGER PRIMARY KEY,
        item TEXT NOT NULL,
        status TEXT NOT NULL DEFAULT 'waiting'
    )
""")

Create rows with INSERT. cursor.lastrowid gives the id the database generated, and executemany() inserts several rows in one call:

Python
cur = db.execute("INSERT INTO repairs (item) VALUES (?)", ("Lamp",))
new_id = cur.lastrowid
db.executemany("INSERT INTO repairs (item) VALUES (?)", [("Kettle",), ("Radio",)])
db.commit()

Read with SELECT, naming the columns you need rather than SELECT *, so code doesn't break when columns are added:

Python
row = db.execute("SELECT item, status FROM repairs WHERE id = ?", (new_id,)).fetchone()
waiting = db.execute("SELECT id, item FROM repairs WHERE status = ? ORDER BY id", ("waiting",)).fetchall()

fetchone() returns a tuple or None; fetchall() returns a list of tuples. Set db.row_factory = sqlite3.Row to access columns by name, as in row["item"].

Update and delete

UPDATE and DELETE change every row that matches the WHERE clause. Without one, they affect the whole table:

Python
cur = db.execute("UPDATE repairs SET status = ? WHERE id = ?", ("ready", 1))
print(cur.rowcount)        # 1 if the repair existed, 0 if not

cur = db.execute("DELETE FROM repairs WHERE status = ?", ("collected",))
print(cur.rowcount, "collected repairs removed")
db.commit()

Check rowcount to find out whether anything was actually changed. An update that matches zero rows isn't an error in SQL, so if your code expects to update repair 99 and it doesn't exist, only rowcount == 0 will tell you.

Before running a destructive statement, run the matching SELECT with the same WHERE clause to see which rows it will affect. Many applications avoid deleting altogether and use a "soft delete" instead, setting a deleted_at column, so records can be restored and history is preserved.

Insert or update in one statement

A common need is "add this record, or update it if it already exists", often called an upsert. SQLite (3.24 and later) and PostgreSQL support it with ON CONFLICT:

Python
db.execute("""
    INSERT INTO stock (item, quantity) VALUES (?, ?)
    ON CONFLICT(item) DO UPDATE SET quantity = quantity + excluded.quantity
""", ("fuse", 5))

The conflict target, item, must have a UNIQUE or PRIMARY KEY constraint. excluded refers to the values the failed insert tried to add. MySQL's equivalent is INSERT ... ON DUPLICATE KEY UPDATE. Doing it in one statement avoids the race in "check with SELECT, then INSERT or UPDATE", where another process could insert the same item between your two statements.

Value binding

Every value from outside the program goes in through placeholders, never through string formatting. The placeholder style belongs to the driver:

DriverPlaceholderExample
sqlite3? or :nameWHERE id = ?
MySQL Connector, PyMySQL%s or %(name)sWHERE id = %s
psycopg (PostgreSQL)%s or %(name)sWHERE id = %s
Python
item = "Lamp'); DROP TABLE repairs; --"
db.execute("INSERT INTO repairs (item) VALUES (?)", (item,))   # stored as plain text

The driver sends the value separately from the SQL, so it can't change the statement's meaning. That's the defence against SQL injection.

Placeholders work only for values. Table names, column names and keywords such as ASC/DESC can't be parameters. If a user chooses the sort column, check it against an allow-list: if column not in {"item", "status"}: raise ValueError. Only then insert it into the SQL text.

Transaction units

A transaction groups statements so they succeed or fail together. sqlite3 opens one automatically before a data-changing statement, and nothing is permanent until commit(). Used as a context manager, a connection commits when the block succeeds and rolls back if it raises:

Python
with db:   # one transaction
    db.execute("UPDATE repairs SET status = ? WHERE id = ?", ("ready", 1))
    db.execute("INSERT INTO notes (repair_id, text) VALUES (?, ?)", (1, "Replaced fuse"))

If the insert fails, the status update is rolled back too, so a repair is never marked ready without its note. Committing after each statement separately could leave the data half-changed after an error.

Note that with db: manages the transaction only; it doesn't close the connection. Call db.close() when finished, or use contextlib.closing(), as explained in the database connections lesson.

Independent practice and a worked check

Task: implement parameterized create, read, update and delete operations for a disposable repair table.

Python
import sqlite3
from contextlib import closing

def add(db, item):
    return db.execute("INSERT INTO repairs (item, status) VALUES (?, 'waiting')", (item,)).lastrowid

def get(db, repair_id):
    return db.execute("SELECT item, status FROM repairs WHERE id = ?", (repair_id,)).fetchone()

def mark_ready(db, repair_id):
    return db.execute("UPDATE repairs SET status = 'ready' WHERE id = ?", (repair_id,)).rowcount == 1

def remove(db, repair_id):
    return db.execute("DELETE FROM repairs WHERE id = ?", (repair_id,)).rowcount == 1

with closing(sqlite3.connect(":memory:")) as db:
    db.execute("CREATE TABLE repairs (id INTEGER PRIMARY KEY, item TEXT, status TEXT)")
    with db:
        lamp = add(db, "Lamp")
        print(mark_ready(db, lamp), get(db, lamp))   # True ('Lamp', 'ready')
        print(remove(db, lamp), get(db, lamp))       # True None

Each function returns something checkable, so callers know whether the operation found its row. For the worked check, the clause that limits the rows affected by a simple UPDATE is WHERE.

Reference and next practice

The sqlite3 documentation covers placeholders, transactions and row factories. Continue with database connections and queries and the SQL course. Practise with the Python exercises and Python quiz.

SQL CRUD with Python Summary

  • Create and read: INSERT adds rows while SELECT retrieves chosen columns and matching records.
  • Update and delete: UPDATE and DELETE operate on all matching rows.
  • Value binding: The placeholder syntax belongs to the selected database driver.
  • Transaction units: Related changes should succeed or fail together inside a suitable transaction.

SQL CRUD with Python Examples

Each example shows the code, the output it produces and what each part does.

Example 1

SQL CRUD with Python in a small program

Python
import sqlite3
with sqlite3.connect(":memory:") as db:
    db.execute("CREATE TABLE repairs (id INTEGER PRIMARY KEY, item TEXT, status TEXT)")
    db.execute("INSERT INTO repairs VALUES (?, ?, ?)", (1, "Lamp", "waiting"))
    db.execute("UPDATE repairs SET status = ? WHERE id = ?", ("ready", 1))
    print(db.execute("SELECT item, status FROM repairs WHERE id = ?", (1,)).fetchone())
Output
('Lamp', 'ready')
Line-by-line explanation
  1. Line 2An in-memory database disappears when the program ends, which is ideal for experiments; with handles the transaction.
  2. Line 4Create: three values bound to three placeholders.
  3. Line 5Update: only the row with id 1 changes.
  4. Line 6Read: the updated row comes back as a tuple.
Example 2

All four operations with row checks

Python
import sqlite3
from contextlib import closing

with closing(sqlite3.connect(":memory:")) as db:
    db.row_factory = sqlite3.Row
    db.execute("CREATE TABLE repairs (id INTEGER PRIMARY KEY, item TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'waiting')")

    with db:
        db.executemany("INSERT INTO repairs (item) VALUES (?)", [("Lamp",), ("Kettle",), ("Radio",)])

    with db:
        changed = db.execute("UPDATE repairs SET status = ? WHERE item = ?", ("ready", "Kettle")).rowcount
        missing = db.execute("UPDATE repairs SET status = ? WHERE id = ?", ("ready", 99)).rowcount
    print("updated:", changed, "| id 99 updated:", missing)

    with db:
        removed = db.execute("DELETE FROM repairs WHERE item = ?", ("Radio",)).rowcount
    print("deleted:", removed)

    for row in db.execute("SELECT id, item, status FROM repairs ORDER BY id"):
        print(row["id"], row["item"], row["status"])
Output
updated: 1 | id 99 updated: 0
deleted: 1
1 Lamp waiting
2 Kettle ready
Line-by-line explanation
  1. Line 5sqlite3.Row lets the loop read columns by name.
  2. Line 13Updating a missing id isn't an error, but rowcount reveals that nothing changed.
  3. Line 20The cursor can be looped over directly instead of calling fetchall().

Common SQL CRUD with Python Mistakes

Running UPDATE without WHERE

Don't
db.execute("UPDATE repairs SET status = ?", ("ready",))   # every repair is now ready
Do
db.execute("UPDATE repairs SET status = ? WHERE id = ?", ("ready", repair_id))

Using a placeholder for a column name

Don't
db.execute("SELECT item FROM repairs ORDER BY ?", (sort_column,))   # sorts by a constant
Do
if sort_column not in {"item", "status"}:
    raise ValueError("unsupported sort column")
db.execute(f"SELECT item FROM repairs ORDER BY {sort_column}")

A placeholder always means a value, so the first query orders by the same text for every row. Validate identifiers against an allow-list instead.

SQL CRUD with Python in Real Projects

CRUD in real applications

Web applications map HTTP requests onto CRUD: POST /repairs inserts, GET /repairs/1 selects, PUT or PATCH updates and DELETE deletes. ORMs such as SQLAlchemy and the Django ORM generate the SQL for you, but the same rules apply underneath: parameters for values, WHERE on updates and deletes, and transactions around related changes.

SQLite itself is far more than a teaching tool. It's built into every Python installation, runs inside phones, browsers and desktop apps, and is a good fit for small websites, command-line tools and local data analysis.

SQL CRUD with Python FAQ

What does CRUD stand for?

Create, Read, Update and Delete: the four basic operations on stored data. In SQL they correspond to INSERT, SELECT, UPDATE and DELETE.

Do I need to install anything to use SQLite in Python?

No. The sqlite3 module is part of the standard library, and the database is a single file (or :memory:). You only need extra software for client-server databases such as MySQL or PostgreSQL.

Why aren't my sqlite3 changes saved?

Changes are made inside a transaction that must be committed. Call db.commit(), or use with db: around the changes, which commits automatically when the block succeeds.

How do I get the id of a newly inserted row?

Read cursor.lastrowid from the cursor returned by execute(): new_id = db.execute("INSERT ...", values).lastrowid. PostgreSQL code usually uses INSERT ... RETURNING id instead.

How do I check whether an UPDATE or DELETE changed anything?

Check cursor.rowcount after the statement. It's the number of rows affected, so 0 means no row matched the WHERE clause.

SQL CRUD with Python Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Which clause limits rows affected by a simple UPDATE?

Multiple choicemedium

Which review concern is valid for sql crud with python?

SQL CRUD with Python Quiz

2 questions to check what you learned.

  1. Which clause limits rows affected by a simple UPDATE?

    • ORDER BY alone
    • WHERE
    • Python ignores the distinction
  2. Which statement correctly describes value binding?

    • The placeholder syntax belongs to the selected database driver.
    • Only the example output matters, not the rule
    • Every operation has the same behavior