Skip to main content

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

Metadata

Release files for betterspread 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for betterspread 1.1.0
File Size Uploaded
betterspread-1.1.0.tar.gz 72.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for betterspread 1.1.0
File Interpreter ABI Platform
betterspread-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 89.4 kB

Release files / betterspread-1.1.0.tar.gz

Download URL betterspread-1.1.0.tar.gz
Size 72.8 kB
Tags Source
SHA-256 checksum
How to use checksums
0051978d9680bf2e4e83c762d8a8d5e962649be8a8f9b6fdaaed9c5dc981fd9d
BLAKE2b-256 checksum
How to use checksums
deda356299ba67f1f82b4199b56bef6f4ec51919bf5179bf4ba22ed2a48f657f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 25, 2026.

Transparency log

Release files / betterspread-1.1.0-py3-none-any.whl

Download URL betterspread-1.1.0-py3-none-any.whl
Size 16.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f68390549b5d20ee06d97a0324160b28ebfcdf980998644ddba9f4a9d2666fba
BLAKE2b-256 checksum
How to use checksums
35a9c339c3123ad798df7272728eefb262476499c8c2be9904eb515700dc4224
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

1.0.6

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page