An async wrapper around gspread with cell and row level functionalities
Project description
betterspread
An async Python wrapper around gspread that gives every cell and row first-class async methods — read, write, clear, style, and delete without ever leaving your async/await flow.
Table of Contents
Why betterspread?
gspread is a great library, but all its methods are synchronous and operate at the spreadsheet level. betterspread wraps it to give you:
| Feature | gspread | betterspread |
|---|---|---|
| Async-native API | ✗ | ✓ |
| Cell as a first-class object | ✗ | ✓ |
| Row as a first-class object | ✗ | ✓ |
Per-cell update / clear / style / delete |
✗ | ✓ |
Per-row update / clear / style / delete |
✗ | ✓ |
| Lazy connection (open on first use) | ✗ | ✓ |
| Load credentials from a file or a dict | ✗ | ✓ |
Requirements
- Python ≥ 3.13
- A Google Cloud service account with the Sheets and Drive APIs enabled
Installation
pip install betterspread
Or with uv:
uv add betterspread
Google API Setup
- Go to the Google Cloud Console.
- Create or select a project, then enable the Google Sheets API and Google Drive API.
- Navigate to APIs & Services → Credentials → Create Credentials → Service Account.
- Open the service account, go to the Keys tab, and download a JSON key — save it as
credentials.json. - Open your Google Sheet, click Share, and give the service account's
client_emailEditor access.
credentials.jsonis listed in.gitignoreby default — never commit it.
Quick Start
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 ---
await cell.clear()
await row.clear()
# --- delete ---
await row.delete()
await cell.delete()
asyncio.run(main())
API Reference
Connection
Authenticates with Google and holds the underlying gspread client.
Connection(
credentials_path: Path | str | None = None,
credentials_dict: dict | None = None,
)
Exactly one of the two arguments must be supplied.
| Argument | Type | Description |
|---|---|---|
credentials_path |
Path | str |
Path to a service-account JSON key file. |
credentials_dict |
dict |
Service-account credentials as a dictionary (useful when loading from an environment variable). |
Raises ValueError if neither argument is provided.
Examples
# From a file
con = Connection(credentials_path="./credentials.json")
# From an environment variable
import json, os
con = Connection(credentials_dict=json.loads(os.environ["GOOGLE_CREDENTIALS"]))
Sheet
An async wrapper around a Google Spreadsheet. The connection is opened lazily — no network call is made until the first async method is called.
Sheet(
sheet_name: str,
connection: Connection,
folder_id: str | None = None,
)
| Argument | Type | Description |
|---|---|---|
sheet_name |
str |
The exact title of the spreadsheet as it appears in Google Drive. |
connection |
Connection |
An authenticated Connection instance. |
folder_id |
str | None |
Optional Drive folder ID to scope the search. |
Methods
await sheet.open()
Opens the remote spreadsheet. Called automatically by all other methods — you only need this if you want to pre-warm the connection.
await sheet.get_tab(tab_name) → Tab
Returns the worksheet named tab_name as a Tab.
tab = await sheet.get_tab("Sheet1")
await sheet.tabs(exclude_hidden=False) → list[Tab]
Returns all worksheet tabs in the spreadsheet.
all_tabs = await sheet.tabs()
visible_tabs = await sheet.tabs(exclude_hidden=True)
Tab
Extends gspread's Worksheet with async helpers. Obtain a Tab via Sheet.get_tab() or Sheet.tabs().
Reading
await tab.values(**kwargs) → list[Row]
Returns every row in the sheet as a list of Row objects.
rows = await tab.values()
for row in rows:
print(row)
await tab.get_row(serial_no) → Row
Returns the row at the given 1-based index.
first_row = await tab.get_row(1)
await tab.get_cell(cell_name, render_option="formatted") → Cell
Fetches a single cell by its A1 address. Both single-letter (B3) and multi-letter (AA10) column labels are supported.
| Argument | Type | Description |
|---|---|---|
cell_name |
str |
A1-notation address, e.g. "B3" or "AA10". |
render_option |
str |
"formatted" (default), "unformatted", or "formula". |
cell = await tab.get_cell("B2")
formula = await tab.get_cell("C1", render_option="formula")
Writing
await tab.append(data, get_row=False) → Row | None
Appends a new row at the bottom of the sheet.
| Argument | Type | Description |
|---|---|---|
data |
list |
A flat list of values to write. |
get_row |
bool |
When True, returns the appended Row. |
await tab.append(["Alice", "30", "Engineer"])
# Get the appended row back
row = await tab.append(["Bob", "25"], get_row=True)
await tab.del_row(start, end=None)
Deletes one or more rows by their 1-based indices.
await tab.del_row(3) # delete row 3
await tab.del_row(3, end=5) # delete rows 3, 4, and 5
await tab.del_cell(start, end=None, shift="up")
Deletes a cell or a rectangular range, shifting the remaining cells.
| Argument | Type | Description |
|---|---|---|
start |
str |
Top-left cell address, e.g. "B2". |
end |
str | None |
Bottom-right cell address for a range. Defaults to start. |
shift |
str |
"up" (default) shifts rows up; "left" shifts columns left. |
await tab.del_cell("B2") # single cell, shift up
await tab.del_cell("B2", shift="left") # single cell, shift left
await tab.del_cell("A1", "C3") # delete a 3×3 range
Row
A list subclass where every item is a Cell. Obtain a Row from Tab.get_row(), Tab.values(), or Tab.append(get_row=True).
Every cell in a row carries:
- its column label (
"A","B","AA", …) - its 1-based row index
- a back-reference to its parent
Row
row = await tab.get_row(1)
print(row[0]) # cell value as a string
print(row[0].label) # "A"
print(row[0].row_index) # 1
Methods
await row.update(values, start="A")
Overwrites the row's values starting from column start.
await row.update(["Alice", "30", "Engineer"])
await row.update(["30"], start="B") # only update column B onward
await row.clear()
Clears all values in the row.
await row.clear()
await row.style(obj)
Applies formatting to every cell in the row.
obj can be a Style instance or a raw gspread_formatting.CellFormat.
from betterspread import Style
await row.style(Style(bold=True, bg_color="#ffe599"))
await row.append_cell(value)
Appends one or more values to the end of the row.
await row.append_cell("new value")
await row.append_cell(["col1", "col2"])
await row.refetch()
Reloads the row's values from the remote spreadsheet.
await row.refetch()
await row.delete()
Deletes the entire row from the sheet.
await row.delete()
Cell
A str subclass — the cell's current value is the string itself. Obtain a Cell from Tab.get_cell() or by indexing a Row.
cell = await tab.get_cell("B2")
# or
cell = row[1]
print(cell) # "hello" (Cell is a str)
print(cell.label) # "B"
print(cell.row_index) # 2
print(cell.cell_index) # 1 (0-based)
print(repr(cell)) # <Cell B2='hello'>
Attributes
| Attribute | Type | Description |
|---|---|---|
label |
str |
Column label, e.g. "A" or "AA". |
row_index |
int |
1-based row number. |
cell_index |
int |
0-based column index within its parent row. |
tab |
Tab |
The Tab this cell belongs to. |
row |
Row | None |
Parent Row, or None when fetched via get_cell(). |
Methods
await cell.update(value, input_format="raw", render_format="formatted") → Cell
Writes a new value and returns an updated Cell instance.
| Argument | Type | Description |
|---|---|---|
value |
Any |
The new value to write. |
input_format |
str |
"raw" (default) or "user_entered". |
render_format |
str |
"formatted" (default), "unformatted", or "formula". |
cell = await cell.update("new value")
cell = await cell.update("=SUM(A1:A10)", input_format="user_entered")
Cellis immutable (it is astr).update()returns a newCell— reassign the variable to keep the updated value.
await cell.clear()
Clears the cell's value.
await cell.clear()
await cell.style(obj)
Applies formatting to this cell.
obj can be a Style instance or a raw gspread_formatting.CellFormat.
await cell.style(Style(bold=True, text_color="#cc0000"))
await cell.delete(shift="left")
Deletes the cell and shifts neighbouring cells.
await cell.delete() # shift left (default)
await cell.delete(shift="up") # shift up
Style
A dataclass that builds a gspread_formatting.CellFormat from simple keyword arguments. Pass a Style to Cell.style() or Row.style().
Style(
bg_color: str = "#ffffff",
text_color: str = "#000000",
horizontal_align: str = "left", # "left" | "center" | "right"
vertical_align: str = "middle", # "top" | "middle" | "bottom"
bold: bool = False,
italic: bool = False,
strikethrough: bool = False,
raw: CellFormat | None = None,
)
When raw is provided all other arguments are ignored and the CellFormat is passed through unchanged.
Examples
from betterspread import Style
# Highlight a header row
header_style = Style(
bg_color="#4a86e8",
text_color="#ffffff",
bold=True,
horizontal_align="center",
)
await row.style(header_style)
# Mark a cell as a warning
await cell.style(Style(bg_color="#fff2cc", italic=True))
# Use a pre-built CellFormat directly
from gspread_formatting import CellFormat, Color
await cell.style(Style(raw=CellFormat(backgroundColor=Color(1, 0.8, 0))))
Development
Setup
git clone https://github.com/shahriyar-alam/betterspread.git
cd betterspread
uv sync --group dev
Running Tests
uv run pytest
Tests cover only the functionality betterspread adds on top of gspread — no real network calls are made.
Project Structure
betterspread/
├── __init__.py # public exports
├── connection.py # Connection — gspread auth wrapper
├── sheet.py # Sheet — async Spreadsheet wrapper
├── tab.py # Tab — async Worksheet wrapper
├── row.py # Row — list subclass
├── cell.py # Cell — str subclass
├── style.py # Style — CellFormat builder
└── utils.py # shared helpers (run_in_executor, column utilities)
tests/
├── test_cell.py
├── test_connection.py
├── test_row.py
├── test_style.py
└── test_utils.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file betterspread-1.0.6.tar.gz.
File metadata
- Download URL: betterspread-1.0.6.tar.gz
- Upload date:
- Size: 69.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c07edda5c23ac51b1d22955576a5787539970fdeb78c1b489a9199ef970aafa
|
|
| MD5 |
56b94e8d97ef5c0d3a4385a67236c6c5
|
|
| BLAKE2b-256 |
b49e2614e1356e5d2733c3af5336babe5a4fb74b81126db449f59466d404d56d
|
Provenance
The following attestation bundles were made for betterspread-1.0.6.tar.gz:
Publisher:
publish.yml on shahriyardx/betterspread
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
betterspread-1.0.6.tar.gz -
Subject digest:
9c07edda5c23ac51b1d22955576a5787539970fdeb78c1b489a9199ef970aafa - Sigstore transparency entry: 1228707524
- Sigstore integration time:
-
Permalink:
shahriyardx/betterspread@5b2f08deb384ba1f8ef3f44b3d0b549d94fc78ac -
Branch / Tag:
refs/tags/v1.0.6 - Owner: https://github.com/shahriyardx
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5b2f08deb384ba1f8ef3f44b3d0b549d94fc78ac -
Trigger Event:
release
-
Statement type:
File details
Details for the file betterspread-1.0.6-py3-none-any.whl.
File metadata
- Download URL: betterspread-1.0.6-py3-none-any.whl
- Upload date:
- Size: 15.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d57d02abbd6f88b6f1853975020b7b2e58c7a3a9753f3251b0fc28de478ed7be
|
|
| MD5 |
328055f546764587a1294d560f89bc60
|
|
| BLAKE2b-256 |
aca8c78f8c0ae3d601f6d0bfae8a4a290193f3f024cf0c413b39707da2471d04
|
Provenance
The following attestation bundles were made for betterspread-1.0.6-py3-none-any.whl:
Publisher:
publish.yml on shahriyardx/betterspread
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
betterspread-1.0.6-py3-none-any.whl -
Subject digest:
d57d02abbd6f88b6f1853975020b7b2e58c7a3a9753f3251b0fc28de478ed7be - Sigstore transparency entry: 1228707536
- Sigstore integration time:
-
Permalink:
shahriyardx/betterspread@5b2f08deb384ba1f8ef3f44b3d0b549d94fc78ac -
Branch / Tag:
refs/tags/v1.0.6 - Owner: https://github.com/shahriyardx
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5b2f08deb384ba1f8ef3f44b3d0b549d94fc78ac -
Trigger Event:
release
-
Statement type: