{"record":{"id":"d9d1825d866dabb4","repo":"tursodatabase/turso","slug":"cannot-operate-on-a-closed-cursor","errorCode":null,"errorMessage":"Cannot operate on a closed cursor","messagePattern":"Cannot operate on a closed cursor","errorType":"exception","errorClass":"ProgrammingError","httpStatus":null,"severity":"error","filePath":"bindings/python/turso/lib.py","lineNumber":811,"sourceCode":"\n    def close(self) -> None:\n        if self._closed:\n            return\n        try:\n            # Finalize any active statement to ensure completion.\n            if self._active_stmt is not None:\n                try:\n                    self._active_stmt.finalize()\n                except Exception:\n                    pass\n        finally:\n            self._active_stmt = None\n            self._active_has_rows = False\n            self._closed = True\n\n    def _ensure_open(self) -> None:\n        if self._closed:\n            raise ProgrammingError(\"Cannot operate on a closed cursor\")\n\n    @property\n    def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | None:\n        return self._description\n\n    @property\n    def lastrowid(self) -> int | None:\n        return self._lastrowid\n\n    @property\n    def rowcount(self) -> int:\n        return self._rowcount\n\n    def _reset_last_result(self) -> None:\n        # Ensure any previous statement is finalized to not leak resources\n        if self._active_stmt is not None:\n            try:\n                self._active_stmt.finalize()","sourceCodeStart":793,"sourceCodeEnd":829,"githubUrl":"https://github.com/tursodatabase/turso/blob/c1e59287258d99b309e362a63f48822256e2f65f/bindings/python/turso/lib.py#L793-L829","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Create a new cursor from the connection and re-run the statement","Move all fetches before the close() call in your control flow","Stop caching cursors; create one per unit of work and let it die with the function"],"exampleFix":"// before\ncur = conn.cursor()\ncur.execute(\"SELECT * FROM t\")\ncur.close()\nrows = cur.fetchall()  # ProgrammingError\n\n// after\ncur = conn.cursor()\ncur.execute(\"SELECT * FROM t\")\nrows = cur.fetchall()\ncur.close()","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"from turso.lib import ProgrammingError\n\ntry:\n    rows = cur.fetchall()\nexcept ProgrammingError as e:\n    if \"closed cursor\" not in str(e):\n        raise\n    cur = conn.cursor()\n    cur.execute(sql)\n    rows = cur.fetchall()","preventionTips":["Fetch all rows before closing the cursor","Keep cursor creation, use, and close inside one function or context block","Treat cursors as single-use objects, not long-lived state"],"tags":["python","db-api","cursor","lifecycle"],"backgroundTag":"use-after-close","analyzedSha":"c1e59287258d99b309e362a63f48822256e2f65f","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}