tursodatabase/turso · error · ProgrammingError

Cannot operate on a closed cursor

Error message

Cannot operate on a closed cursor

What it means

Every public method on the blocking Cursor (execute, executemany, fetchone, fetchmany, fetchall, ...) first calls _ensure_open(). Once cursor.close() has run, the _closed flag is set in a finally block and every subsequent call raises ProgrammingError('Cannot operate on a closed cursor'). The flag is set even if finalizing the active statement inside close() failed, so close() always wins.

Solutions

  1. Create a new cursor from the connection and re-run the statement
  2. Move all fetches before the close() call in your control flow
  3. Stop caching cursors; create one per unit of work and let it die with the function

Example fix

// before
cur = conn.cursor()
cur.execute("SELECT * FROM t")
cur.close()
rows = cur.fetchall()  # ProgrammingError

// after
cur = conn.cursor()
cur.execute("SELECT * FROM t")
rows = cur.fetchall()
cur.close()
Defensive patterns

Strategy: try-catch

Try / catch

from turso.lib import ProgrammingError

try:
    rows = cur.fetchall()
except ProgrammingError as e:
    if "closed cursor" not in str(e):
        raise
    cur = conn.cursor()
    cur.execute(sql)
    rows = cur.fetchall()

Prevention

When it happens

Trigger: Calling any cursor method after cursor.close(); reusing a cursor stored on an object that was cleaned up earlier; a generator or lazy iterator that resumes after the code that owned the cursor called close().

Common situations: Cleanup in a finally block that runs before the last fetch; one cursor cached as an instance attribute and closed by a helper method; test teardown closing cursors while assertions still read them.

Related errors


AI-assisted analysis of tursodatabase/turso@c1e5928725 (2026-08-20). Data as JSON: /api/errors/d9d1825d866dabb4. Report an issue: GitHub.

Appendix: source

Thrown at bindings/python/turso/lib.py:811

    def close(self) -> None:
        if self._closed:
            return
        try:
            # Finalize any active statement to ensure completion.
            if self._active_stmt is not None:
                try:
                    self._active_stmt.finalize()
                except Exception:
                    pass
        finally:
            self._active_stmt = None
            self._active_has_rows = False
            self._closed = True

    def _ensure_open(self) -> None:
        if self._closed:
            raise ProgrammingError("Cannot operate on a closed cursor")

    @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 _reset_last_result(self) -> None:
        # Ensure any previous statement is finalized to not leak resources
        if self._active_stmt is not None:
            try:
                self._active_stmt.finalize()

View on GitHub (pinned to c1e5928725)