Fixing Python sqlite3 That Executes INSERT but Never Persists Data to Disk
You run your script, watch the INSERT statements execute without a single error, even do a quick SELECT before the process exits — the rows are sitting right there. Then you open the database file in another tool, or re-run the script, and the table is empty. This is one of the most disorienting bugs in Python's sqlite3 module, and it is almost always caused by one of four very specific mistakes.
This guide walks through each cause in order of likelihood, with runnable code you can drop into your project to verify and fix the problem immediately.
What You'll Learn
- How SQLite's transaction model works under the hood and why Python wraps it differently from other database drivers
- Why missing a single
connection.commit()call silently discards every write in your session - What
isolation_level=Nonemeans and when it is actually the right setting - How to use the connection as a context manager so commit and rollback happen automatically
- How to avoid accidentally writing to an in-memory database that evaporates when your script exits
Prerequisites
You need Python 3.6 or later (the sqlite3 module ships with every standard CPython build, so no extra installs are required). Examples assume you already have a working script that opens a connection, runs an INSERT, and closes — you just can't find your data afterward.
How SQLite Transactions Actually Work
SQLite is not like a client-server database where every statement auto-commits by default. SQLite wraps your statements in transactions, and a transaction must be explicitly committed before its changes are written to the on-disk journal and the main database file.
Python's sqlite3 module adds another layer on top of that. Depending on the isolation_level setting you pass to sqlite3.connect(), the module may automatically open an implicit transaction before any Data Manipulation Language (DML) statement — meaning INSERT, UPDATE, DELETE, or REPLACE. That implicit transaction will not be committed unless you call connection.commit() or the connection object is closed in a way that triggers a commit.
Closing the connection with connection.close() does not automatically commit an open transaction. Python rolls it back silently instead. That one sentence explains most cases of disappearing data.
The Most Common Culprit: Forgetting to Call commit()
This is the root cause in the overwhelming majority of reports. The script runs without raising any exceptions, but the transaction was never committed before the connection closed.
Here is a minimal broken example:
import sqlite3
conn = sqlite3.connect("mydata.db")
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
# No commit() call here!
conn.close() # Python rolls back the open transaction silently
When you reopen mydata.db, the users table exists (DDL like CREATE TABLE is handled differently in SQLite — it auto-commits in most configurations), but the row for Alice is gone.
The fix is straightforward: call conn.commit() after every logical batch of writes you want to preserve.
import sqlite3
conn = sqlite3.connect("mydata.db")
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
conn.commit() # Changes are now written to disk
conn.close()
If your script does many inserts in a loop, committing once at the end is fine for correctness (and actually faster than committing per-row). Only commit inside the loop if you need each row to be durable immediately — for example, when processing a long-running job that could be interrupted.
For more on data integrity issues with sqlite3, see how duplicate rows appear on every script run — a related problem that is also usually a transaction management mistake.
Isolation Level Set to None Behaves Differently Than You Think
Python's sqlite3.connect() accepts an isolation_level parameter. When you set it to None, you are telling the module to operate in autocommit mode — every statement is committed immediately without needing an explicit commit() call.
This sounds like the easy fix, but it introduces a subtle trap in the opposite direction. If you mix isolation_level=None with manual BEGIN statements, you own the transaction management entirely. Forgetting a COMMIT in that case still loses your data.
import sqlite3
# Autocommit mode — every statement commits immediately
conn = sqlite3.connect("mydata.db", isolation_level=None)
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
# No commit() needed — already on disk
conn.close()
The default isolation_level is an empty string (""), which means the module issues an implicit BEGIN before DML statements. Most developers should stay with the default and call commit() explicitly — it gives you transactional safety and makes the intent clear in code review.
If you have ever been confused by sqlite3 returning stale data after a commit, isolation level is often the underlying reason there too.
Using the Connection as a Context Manager (the Right Way)
Python's sqlite3 connection object supports the context manager protocol, and this is the cleanest pattern for ensuring commits and rollbacks happen correctly without scattering commit() calls throughout your code.
There is one important thing to know before you use it: with conn: does NOT close the connection when the block exits. It only commits on success or rolls back on exception. You still need conn.close() afterward if you want to release the file handle.
import sqlite3
conn = sqlite3.connect("mydata.db")
try:
with conn: # Commits on success, rolls back on exception
conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
conn.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
conn.execute("INSERT INTO users (name) VALUES (?)", ("Bob",))
finally:
conn.close()
This pattern is especially useful when inserting related rows that must succeed or fail together. If the second INSERT raises an exception, the first one is rolled back automatically — no partial writes hit the disk.
For bulk inserts, executemany inside a with conn: block is both safe and fast:
import sqlite3
rows = [("Alice",), ("Bob",), ("Carol",)]
conn = sqlite3.connect("mydata.db")
try:
with conn:
conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
conn.executemany("INSERT INTO users (name) VALUES (?)", rows)
finally:
conn.close()
Accidentally Writing to an In-Memory Database
If you pass ":memory:" as the database path, SQLite creates a database entirely in RAM. It is fast and disposable — the moment the connection closes, every byte is gone. This is intentional for testing, but it is easy to introduce by accident.
Common scenarios where this happens:
- A configuration file or environment variable supplies the database path, and the value is missing or empty — your code falls back to
":memory:"as a default. - A unit test sets
":memory:"and you forget to revert the change before running against real data. - A constant named
DB_PATHis defined in two places and the wrong one is imported.
Add a quick assertion at startup to rule this out:
import sqlite3
import os
DB_PATH = os.getenv("DB_PATH", "mydata.db")
assert DB_PATH != ":memory:", "In-memory database will not persist — check your environment variables"
conn = sqlite3.connect(DB_PATH)
For debugging, print the actual path and verify it resolves to the file you expect:
import pathlib
db_file = pathlib.Path(DB_PATH).resolve()
print(f"Connecting to: {db_file}")
assert db_file.exists() or True # File may not exist yet on first run — that's fine
Checking That You're Opening the Right File Path
SQLite creates a new, empty database file if the path you specify does not already exist. If you later open a slightly different path — a typo, a different working directory, a relative vs. absolute path mismatch — SQLite quietly creates a second empty file and you never see your original data.
This is especially common in scripts that use relative paths and are run from different directories. The fix is to anchor the database path to a known location:
import sqlite3
import pathlib
# Always resolve relative to the script file, not the working directory
BASE_DIR = pathlib.Path(__file__).parent
DB_PATH = BASE_DIR / "mydata.db"
conn = sqlite3.connect(str(DB_PATH))
You can also add a quick sanity check after an INSERT to confirm the row count is what you expect:
with conn:
conn.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM users")
print("Row count:", cursor.fetchone()[0])
If the row count is correct here but zero when you open the file separately, the path mismatch is almost certainly the culprit.
File-path issues can interact with other sqlite3 problems. If you are running concurrent write operations and seeing errors alongside missing data, the guide on fixing sqlite3 OperationalError on concurrent writes covers the locking model that determines which process actually owns the file.
Common Pitfalls to Watch For
A few additional traps that do not fit neatly into the categories above:
- Exception swallowed before commit: If an exception is raised after your INSERTs but before
commit(), and you catch it without re-raising, the writes are lost. Always structure your error handling so a failed path does not silently skip the commit or explicitly triggers a rollback. - Multiple connections to the same file: If two connections have the same file open and one commits while the other still has a transaction open, the second connection reads its own (not-yet-committed) snapshot. The data is not lost, but a SELECT in the second connection will not show the first connection's rows until the second connection starts a new transaction. This is standard SQLite isolation behavior, not a bug.
- WAL mode and checkpoint timing: If you have enabled Write-Ahead Logging (
PRAGMA journal_mode=WAL), committed data sits in the WAL file before a checkpoint moves it to the main database. The data is safe and readable through any connection, but if you inspect the raw.dbfile with a hex editor before the checkpoint you will not see the rows there yet. This is not data loss. - Threads sharing a connection without locking: The default
sqlite3connection object is not thread-safe. If multiple threads share one connection and one thread commits while another is mid-INSERT, the behavior is undefined. Usecheck_same_thread=Falseonly when you protect the connection with athreading.Lock.
If your data loss involves unusual characters surviving in-session but disappearing on disk, the cause might be encoding rather than transaction issues — the post on sqlite3 losing data when writing Unicode or emoji characters covers that specific failure mode.
Wrapping Up: Data Persistence Checklist
When your sqlite3 INSERTs vanish, run through this list in order:
- Call
conn.commit()after every batch of writes you want to keep. If you usewith conn:as a context manager, the commit happens automatically on a clean exit. - Check your
isolation_level. The default ("") requires manual commits.Nonegives you autocommit but removes implicit transaction safety. - Verify you are not connecting to
":memory:"by printing the resolved path at startup during debugging. - Pin your database path to an absolute location using
pathlib.Path(__file__).parentso the working directory can never shift it. - Audit your exception handling to confirm no code path swallows an error and then skips the commit silently.
Most cases resolve at step one. If you make it to step five without a fix, add a SELECT COUNT(*) assertion immediately after commit() to confirm the rows are present in that connection's view — that narrows the problem to either the path or the isolation model rather than the commit logic itself.
Frequently Asked Questions
Why does Python sqlite3 INSERT run without error but the data is gone when I reopen the database?
Python's sqlite3 module does not auto-commit by default. When you call connection.close() without first calling connection.commit(), any open transaction is silently rolled back and your writes are discarded. Always call conn.commit() before closing, or use the connection as a context manager with 'with conn:' to commit automatically.
Does closing a sqlite3 connection automatically commit the transaction in Python?
No, it does not. Calling conn.close() on an open transaction triggers a rollback, not a commit. You must call conn.commit() explicitly before closing, or use the context manager pattern 'with conn:' which commits on success and rolls back on exception.
What does isolation_level=None do in Python sqlite3 and does it fix missing data?
Setting isolation_level=None enables autocommit mode, meaning every SQL statement is committed to disk immediately without needing an explicit commit() call. It will prevent the missing-commit problem, but it removes transactional safety — if one of several related inserts fails, there is nothing to roll back the earlier ones.
Can an in-memory SQLite database accidentally cause data to disappear between script runs?
Yes. If your database path resolves to ':memory:', SQLite stores everything in RAM and the data is completely gone the moment the connection closes. Double-check your DB_PATH environment variable or configuration and print the resolved path at startup to rule this out.
How do I make sure sqlite3 writes are durable across multiple inserts in a loop?
Wrap the entire loop in a single 'with conn:' block or call conn.commit() once after the loop finishes. Committing per-row is safe but much slower; a single commit at the end of a batch is both correct and significantly faster for bulk inserts.
📤 Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!