| Row | A | B | C |
|---|---|---|---|
| 1 | betterspread | ||
| 2 | Every cell and row, first-class and async. | ||
| 3 | An async Python wrapper around gspread that gives every cell and row real async methods — read, write, clear, style and delete without ever leaving your | ||
| 4 | Python ≥ 3.10 · or | ||
Why betterspread?
gspread is a great library — but it’s synchronous, and it works at the spreadsheet level.
| Row | A · Feature | B · gspread | C · betterspread |
|---|---|---|---|
| 1 | Async-native API | ✗Not supported | ✓Supported |
| 2 | Cell as a first-class object | ✗Not supported | ✓Supported |
| 3 | Row as a first-class object | ✗Not supported | ✓Supported |
| 4 | Per-cell update / clear / style / delete | ✗Not supported | ✓Supported |
| 5 | Per-row update / clear / style / delete | ✗Not supported | ✓Supported |
| 6 | Lazy connection (opens on first use) | ✗Not supported | ✓Supported |
| 7 | Credentials from a file or a dict | ✗Not supported | ✓Supported |
| 8 | Automatic retry/backoff on rate limits | ✗Not supported | ✓Supported |
| 9 | Numeric cell accessor (cell.number) | ✗Not supported | ✓Supported |
Quick start
Point it at a service-account key, name your sheet, and await.
import asyncio
from betterspread import Connection, Sheet
async def main():
con = Connection(credentials_path="./credentials.json")
sheet = Sheet(connection=con, sheet_name="My Spreadsheet")
tab = await sheet.get_tab("Sheet1")
# --- read ---
rows = await tab.values() # list[Row]
row = await tab.get_row(1) # Row (1-based)
cell = await tab.get_cell("B2") # Cell
print(cell) # Cell is a plain str subclass
print(row[0].label, row[0]) # "A" "hello"
# --- write ---
cell = await cell.update("world")
await row.update(["Alice", "30", "Engineer"])
# --- append ---
await tab.append(["Bob", "25", "Designer"])
# --- clear / delete ---
await cell.clear()
await row.delete()
asyncio.run(main())The objects
Six of them. That’s the whole library.
| Row | A · Object | B · What it is | C · Methods |
|---|---|---|---|
| 1 | Connection | Authenticates with Google and holds the gspread client. Connection(credentials_path=..., credentials_dict=...) |
|
| 2 | Sheet | A spreadsheet. Opens lazily — no network call until the first await. Sheet(sheet_name, connection, folder_id=None) |
|
| 3 | Tab | A worksheet. Reads return Rows and Cells, not raw lists. tab = await sheet.get_tab('Sheet1') |
|
| 4 | Row | A list subclass — index it like a list, await methods on it. row = await tab.get_row(1) |
|
| 5 | Cell | A str subclass — use it as a string, or read cell.number. cell = await tab.get_cell('B2') |
|
| 6 | Style | Formatting, without touching gspread_formatting directly. Style(bg_color='#fff', bold=True, ...) |
|
Google API setup
One-time, and you need a service account.
| 1 | Open the Google Cloud Console, create or select a project. |
| 2 | Enable the Google Sheets API and the Google Drive API. |
| 3 | APIs & Services → Credentials → Create Credentials → Service Account. |
| 4 | Open the service account → Keys → download a JSON key as credentials.json. |
| 5 | Share your Sheet with the service account's client_email as an Editor. |
Keep the key out of git. credentials.json is in .gitignore by default — never commit it. Loading from an env var works too: Connection(credentials_dict=json.loads(os.environ["GOOGLE_CREDENTIALS"]))
betterspread runs gspread’s blocking calls in a thread pool and retries transient 429/5xx errors with exponential backoff. Size the pool with BETTERSPREAD_MAX_WORKERS.