{"record":{"id":"46a9470e26576c89","repo":"tursodatabase/turso","slug":"cannot-operate-on-a-closed-cursor-46a947","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":"serverless/python/turso_serverless/connection.py","lineNumber":204,"sourceCode":"    @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 close(self) -> None:\n        self._closed = True\n        self._rows = []\n\n    def _ensure_open(self) -> None:\n        if self._closed:\n            raise ProgrammingError(\"Cannot operate on a closed cursor\")\n\n    @staticmethod\n    def _convert_params(\n        parameters: Sequence[Any] | Mapping[str, Any],\n    ) -> tuple[Optional[list], Optional[list[tuple[str, Any]]]]:\n        \"\"\"Convert DB-API parameters to protocol args/named_args.\"\"\"\n        if isinstance(parameters, Mapping):\n            named = []\n            for key, val in parameters.items():\n                # Try :name, $name, @name prefixes\n                if isinstance(key, str) and not key.startswith((\":\", \"$\", \"@\")):\n                    named.append((f\":{key}\", val))\n                else:\n                    named.append((key, val))\n            return None, named\n        params = list(parameters) if parameters else []\n        return params if params else None, None\n","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/serverless/python/turso_serverless/connection.py#L186-L222","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ncur = conn.cursor()\ncur.execute(\"SELECT 1\")\ncur.close()\nrows = cur.fetchall()  # ProgrammingError: closed cursor\n\n// after\ncur = conn.cursor()\ncur.execute(\"SELECT 1\")\nrows = cur.fetchall()\ncur.close()","handlingStrategy":"validation","validationCode":"def fetch_all_then_close(cur):\n    \"\"\"Drain rows before closing; close is always last.\"\"\"\n    try:\n        return cur.fetchall()\n    finally:\n        if not getattr(cur, \"_closed\", False):\n            cur.close()","typeGuard":null,"tryCatchPattern":"from turso_serverless.dbapi import ProgrammingError\n\ntry:\n    cur.execute(sql, params)\nexcept ProgrammingError as e:\n    if \"closed cursor\" not in str(e):\n        raise\n    cur = conn.cursor()  # parent connection may still be open\n    cur.execute(sql, params)","preventionTips":["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"],"tags":["python","db-api","cursor","lifecycle","use-after-close"],"backgroundTag":"resource-used-after-close","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}