SQLite Objects Created in a Thread: Keep Connection Ownership in the Worker
By ON Lab · Sources checked November 8, 2026
Python’s default SQLite connection belongs to the thread that created it. If a worker uses a connection made by another thread, ProgrammingError can occur even when the underlying SQLite library supports threaded operation. Record where the connection is created and used. For an independent worker task, creating and closing a connection inside that worker avoids crossing this boundary.
Separate the connection from the database file
A connection is a live Python object with transaction state. A database file is the persistent data it opens. Passing a filename to a worker differs from passing a connection object. sqlite3.threadsafety describes module-level support based on library configuration; it does not override the connection’s check_same_thread setting.
A minimal failing boundary
import sqlite3
import threading
connection = sqlite3.connect(":memory:")
def worker():
connection.execute("SELECT 1").fetchone()
thread = threading.Thread(target=worker)
thread.start()
thread.join()
connection.close()In our Python 3.9.6 fixture, the worker raised ProgrammingError saying SQLite objects created in one thread could only be used in that same thread. The thread identifiers in the message are specific to the run. Capture worker exceptions explicitly: joining a thread does not automatically raise its exception in the caller.
Open the connection in the worker
This example uses a disposable file, not a production database. It commits a row before reading it from a new worker connection. A result queue lets the caller inspect success or failure.
from pathlib import Path
from queue import Queue
import sqlite3
import tempfile
import threading
with tempfile.TemporaryDirectory() as temporary:
database = Path(temporary) / "sample.sqlite"
setup = sqlite3.connect(database)
try:
setup.execute("CREATE TABLE item (value TEXT)")
setup.execute("INSERT INTO item VALUES (?)", ("sample",))
setup.commit()
finally:
setup.close()
result = Queue()
def worker():
connection = None
try:
connection = sqlite3.connect(database)
value = connection.execute("SELECT value FROM item").fetchone()
result.put(("ok", value))
except Exception as error:
result.put(("error", type(error).__name__, str(error)))
finally:
if connection is not None:
connection.close()
thread = threading.Thread(target=worker)
thread.start()
thread.join()
print(result.get())The worker-owned connection pattern returned the inserted value sample in our local fixture. Committing first is part of the comparison: independent connections do not make uncommitted writes visible. Closing in the worker also makes ownership easy to audit.
Why not only disable the check?
check_same_thread=False disables the default thread-use check. It does not design transaction ownership, application ordering or write serialization. Python’s documentation calls out possible user-side serialization for writes. A lock around individual statements may still be insufficient when a multi-statement transaction must stay together.
Another design is one database-owning worker receiving complete queued jobs. A connection per independent worker may be simpler for other applications. Neither guarantees unlimited concurrent writing or automatically fits a framework that moves callbacks between threads. Inspect the framework’s lifecycle before changing a global connection option.
Follow the next symptom precisely
| Observation | Next investigation |
|---|---|
| A lock error replaces the thread error | Inspect transaction duration, write concurrency and cleanup. |
| A new :memory: connection has no table | Ordinary separate in-memory connections have separate databases. Use a deliberate test file for independent-connection comparisons. |
| A row is absent on another connection | Check commit and read-transaction state before assuming data loss. |
| The thread ends but its result is missing | Return and inspect its exception, not only whether it ended. |
This addresses connection/thread ownership. For foreign-key enforcement after transaction configuration changes, use the separate foreign-key initialization check. Changing thread checks does not repair a mistimed pragma.
Method: Python 3.13 documentation, a CPython wrapper/library discussion, and original Python 3.9.6 fixtures using SQLite 3.54.0 on macOS. We reproduced a cross-thread read failure and a worker-owned read; no concurrent-write stress test, framework or production database was used.