{"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":565,"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":547,"sourceCodeEnd":583,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/bindings/python/turso/lib.py#L547-L583","documentation":"Every Cursor method calls _ensure_open, which raises DB-API ProgrammingError once _closed is True. A cursor becomes closed after an explicit cursor.close(); any subsequent execute/fetch/description access fails. The check is on the cursor itself, so it fires even if the parent connection is still open.","triggerScenarios":"Calling execute()/fetchone()/fetchall()/executemany() after cur.close(); reusing a cursor stored on an object (request handler, service class) from a previous cycle; consuming an iterator after close; partial fetch then close then fetch again.","commonSituations":"Caching cursors for reuse instead of creating fresh ones per operation; cleanup code closing cursors while background tasks still hold references; loops that close inside the body but continue iterating.","solutions":["Create a new cursor for each unit of work instead of reusing closed ones: cur = conn.cursor()","Finish all fetches before calling close(); treat close() as the last operation on the object","Restructure so each owner creates, uses, and closes its own cursor within one scope","Catch ProgrammingError with this message as a defensive signal of a lifecycle bug, then log and recreate"],"exampleFix":"# before\ncur = conn.cursor()\ncur.execute(\"SELECT id FROM t\")\nfirst = cur.fetchone()\ncur.close()\nrest = cur.fetchall()  # ProgrammingError: Cannot operate on a closed cursor\n\n# after\ncur = conn.cursor()\ncur.execute(\"SELECT id FROM t\")\nrows = cur.fetchall()\ncur.close()","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    rows = cur.fetchall()\nexcept ProgrammingError as e:\n    if \"closed cursor\" in str(e):\n        cur = conn.cursor()          # lifecycle bug signal: recreate and retry once\n        cur.execute(last_sql, last_params)\n        rows = cur.fetchall()\n    else:\n        raise","preventionTips":["One cursor per unit of work; create, use, close in the same scope","Never store cursors on long-lived objects (handlers, services) for later reuse","Close is terminal: complete all fetches first, including exhausting iterators","Cursor has no context manager — use try/finally to guarantee close ordering"],"tags":["python","lifecycle","cursor","db-api","use-after-close"],"backgroundTag":"use-after-close","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}