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.1.tar.gz (36.3 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.1-py3-none-any.whl (15.3 kB view details)

Uploaded Python 3

File details

Details for the file betterspread-1.0.1.tar.gz.

File metadata

  • Download URL: betterspread-1.0.1.tar.gz
  • Upload date:
  • Size: 36.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for betterspread-1.0.1.tar.gz
Algorithm Hash digest
SHA256 ccb19c981a0a862bbc91ac12cf5377a671433b7fb48ff64c109040c3145540b8
MD5 6710e134fd5f862b4ad2b9b262b1501c
BLAKE2b-256 5fdeaf37133d6a6cb422ae24edd85a78eb9f6a981978844c1e2fff5f79a1ab68

See more details on using hashes here.

File details

Details for the file betterspread-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: betterspread-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 15.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.1 {"installer":{"name":"uv","version":"0.11.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for betterspread-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0b30840512744019d400f57747a656e1164fa880d863acea1503a1aa16a1f596
MD5 01ca7c626b730742eb2876c134bc24c0
BLAKE2b-256 4f3e3585e95914d5df6928f97f2bc12622bbe0f09b5939c9058aeb0e820311b6

See more details on using hashes here.

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