Skip to main content

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

  1. Go to the Google Cloud Console.
  2. Create or select a project, then enable the Google Sheets API and Google Drive API.
  3. Navigate to APIs & Services → Credentials → Create Credentials → Service Account.
  4. Open the service account, go to the Keys tab, and download a JSON key — save it as credentials.json.
  5. Open your Google Sheet, click Share, and give the service account's client_email Editor access.

credentials.json is listed in .gitignore by 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")

Cell is immutable (it is a str). update() returns a new Cell — 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

betterspread-1.0.6.tar.gz (69.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

betterspread-1.0.6-py3-none-any.whl (15.0 kB view details)

Uploaded Python 3

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

Hashes for betterspread-1.0.6.tar.gz
Algorithm Hash digest
SHA256 9c07edda5c23ac51b1d22955576a5787539970fdeb78c1b489a9199ef970aafa
MD5 56b94e8d97ef5c0d3a4385a67236c6c5
BLAKE2b-256 b49e2614e1356e5d2733c3af5336babe5a4fb74b81126db449f59466d404d56d

See more details on using hashes here.

Provenance

The following attestation bundles were made for betterspread-1.0.6.tar.gz:

Publisher: publish.yml on shahriyardx/betterspread

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for betterspread-1.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 d57d02abbd6f88b6f1853975020b7b2e58c7a3a9753f3251b0fc28de478ed7be
MD5 328055f546764587a1294d560f89bc60
BLAKE2b-256 aca8c78f8c0ae3d601f6d0bfae8a4a290193f3f024cf0c413b39707da2471d04

See more details on using hashes here.

Provenance

The following attestation bundles were made for betterspread-1.0.6-py3-none-any.whl:

Publisher: publish.yml on shahriyardx/betterspread

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page