{"record":{"id":"265e4595cd7000e8","repo":"tursodatabase/turso","slug":"keyerror-key","errorCode":null,"errorMessage":"KeyError(key)","messagePattern":"KeyError\\(key\\)","errorType":"exception","errorClass":"KeyError","httpStatus":null,"severity":"error","filePath":"bindings/python/turso/lib.py","lineNumber":1168,"sourceCode":"        obj._data = data\n        # Build mapping from column name to index\n        desc = cursor.description or ()\n        obj._keys = tuple(col[0] for col in desc)\n        obj._index = {name: idx for idx, name in enumerate(obj._keys)}\n        return obj\n\n    def keys(self) -> list[str]:\n        return list(self._keys)\n\n    def __getitem__(self, key: int | str | slice, /) -> Any:\n        if isinstance(key, slice):\n            return self._data[key]\n        if isinstance(key, int):\n            return self._data[key]\n        # key is column name\n        idx = self._index.get(key)\n        if idx is None:\n            raise KeyError(key)\n        return self._data[idx]\n\n    def __hash__(self) -> int:\n        return hash((self._keys, self._data))\n\n    def __iter__(self) -> Iterator[Any]:\n        return iter(self._data)\n\n    def __len__(self) -> int:\n        return len(self._data)\n\n    def __eq__(self, value: object, /) -> bool:\n        if not isinstance(value, Row):\n            return NotImplemented  # type: ignore[return-value]\n        return self._keys == value._keys and self._data == value._data\n\n    def __ne__(self, value: object, /) -> bool:\n        if not isinstance(value, Row):","sourceCodeStart":1150,"sourceCodeEnd":1186,"githubUrl":"https://github.com/tursodatabase/turso/blob/c1e59287258d99b309e362a63f48822256e2f65f/bindings/python/turso/lib.py#L1150-L1186","documentation":"Row.__getitem__ resolves string keys through an index built from the result set's column names; integer and slice keys go straight to the underlying data tuple. A string that is not one of the selected column names raises KeyError(key). The row only knows the names the SELECT actually returned, not the full table schema.","triggerScenarios":"row['typo'] after a normal query; a name that differs from the SELECT list, such as an unaliased aggregate or expression column; a column dropped or renamed by a schema migration while the code still selects *.","commonSituations":"SELECT * across a migration that renames a column; joins returning one bare name for two same-named columns; trusting keys from external JSON instead of row.keys(); case mismatches with the query text.","solutions":["Print row.keys() once and use the exact names the query returned","Alias computed columns in the SELECT list (SELECT count(*) AS n FROM t)","Select explicit columns instead of * so the name set is stable","Access by integer index when names are unstable"],"exampleFix":"# before\ncur.execute(\"SELECT count(*) FROM t\")\nrow = cur.fetchone()\nrow[\"total\"]  # KeyError('total')\n\n# after\ncur.execute(\"SELECT count(*) AS total FROM t\")\nrow = cur.fetchone()\nrow[\"total\"]  # works","handlingStrategy":"validation","validationCode":"def column_or(row, name, default=None):\n    return row[name] if name in row.keys() else default","typeGuard":"def has_column(row, name: str) -> bool:\n    return name in row.keys()","tryCatchPattern":"try:\n    value = row[\"total\"]\nexcept KeyError:\n    value = row[row.keys()[0]]  # or log row.keys() and fix the SELECT","preventionTips":["Log row.keys() once after execute when wiring up new queries","Alias every expression column in the SELECT list","Select explicit columns instead of * when code reads by name"],"tags":["python","row-access","keyerror","column-names"],"backgroundTag":"unknown-column-name","analyzedSha":"c1e59287258d99b309e362a63f48822256e2f65f","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}