tursodatabase/turso · error · ProgrammingError
Cannot operate on a closed cursor
Error message
Cannot operate on a closed cursor
What it means
ProgrammingError raised by Cursor._ensure_open() (connection.py:202-204) when execute(), executemany(), executescript(), fetchone(), fetchmany(), fetchall(), or iteration runs on a cursor after Cursor.close(). close() also clears the buffered row list, so there is no way to read leftover rows afterwards. Unlike a closed Connection, this is cheap to recover from: the parent connection may still be open and can mint a new cursor.
Source
Thrown at serverless/python/turso_serverless/connection.py:204
@property
def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | None:
return self._description
@property
def lastrowid(self) -> int | None:
return self._lastrowid
@property
def rowcount(self) -> int:
return self._rowcount
def close(self) -> None:
self._closed = True
self._rows = []
def _ensure_open(self) -> None:
if self._closed:
raise ProgrammingError("Cannot operate on a closed cursor")
@staticmethod
def _convert_params(
parameters: Sequence[Any] | Mapping[str, Any],
) -> tuple[Optional[list], Optional[list[tuple[str, Any]]]]:
"""Convert DB-API parameters to protocol args/named_args."""
if isinstance(parameters, Mapping):
named = []
for key, val in parameters.items():
# Try :name, $name, @name prefixes
if isinstance(key, str) and not key.startswith((":", "$", "@")):
named.append((f":{key}", val))
else:
named.append((key, val))
return None, named
params = list(parameters) if parameters else []
return params if params else None, None
View on GitHub (pinned to bad083fafb)
Solutions
- Create a fresh cursor with conn.cursor() (or use conn.execute(sql) for one-shot statements)
- Never close cursors you did not create — let the owner close them
- Fetch all rows first, then close: cur.close() must be the last statement touching the cursor
Example fix
// before
cur = conn.cursor()
cur.execute("SELECT 1")
cur.close()
rows = cur.fetchall() # ProgrammingError: closed cursor
// after
cur = conn.cursor()
cur.execute("SELECT 1")
rows = cur.fetchall()
cur.close() Defensive patterns
Strategy: validation
Validate before calling
def fetch_all_then_close(cur):
"""Drain rows before closing; close is always last."""
try:
return cur.fetchall()
finally:
if not getattr(cur, "_closed", False):
cur.close() Try / catch
from turso_serverless.dbapi import ProgrammingError
try:
cur.execute(sql, params)
except ProgrammingError as e:
if "closed cursor" not in str(e):
raise
cur = conn.cursor() # parent connection may still be open
cur.execute(sql, params) Prevention
- Scope each cursor to one function; never close cursors passed in as arguments
- Use conn.execute(sql) for one-shot statements — it mints its own cursor each call
- Fetch everything you need before calling close(); close() also drops buffered rows
When it happens
Trigger: cur.close() followed by cur.execute(), cur.fetchone(), or 'for row in cur'. Common shapes: a helper closes a cursor it received as a parameter and the caller keeps using it; a retry loop that closes the cursor at the end of each attempt but re-enters with the same cursor; storing a cursor in a long-lived object and closing it during cleanup.
Common situations: Mixing conn.execute() (which creates and returns a fresh cursor each call) with manually managed cursors and closing the wrong one; porting sqlite3 code where cursors are cheap and short-lived; cleanup code in generators that closes before the consumer finishes iterating.
Related errors
- Cannot operate on a closed cursor
- Cannot operate on a closed cursor
- Cannot operate on a closed connection
- Cannot operate on a closed connection
- Database is closed
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/46a9470e26576c89.
Report an issue: GitHub.