Scan Google Sheet
Read public Google Sheets into Polars DataFrames and LazyFrames — no auth, no service accounts, no API keys.
🦀 Now powered by Rust
This is a from-scratch rewrite of the original pure-Python scan-google-sheet. The
public API is unchanged — read_google_sheet, scan_google_sheet, and the same
exception hierarchy all work exactly as before — but the fetch-and-parse core
underneath is now Rust, compiled to a native extension via PyO3
and maturin.
| Before (0.1.x) | Now (0.2.0) | |
|---|---|---|
| HTTP client | httpx (Python) |
reqwest (Rust, blocking + rustls) |
| CSV parsing | polars (Python API) |
polars (Rust, native) |
| GIL behaviour | Held during fetch + parse | Released for the whole fetch + parse |
| Distribution | Pure-Python wheel | Compiled extension module (_core) per platform |
| Public API | read_google_sheet, scan_google_sheet |
Unchanged — drop-in upgrade |
| Errors | Python exception hierarchy | Same hierarchy, raised from Rust |
Why it's faster: the HTTP request and CSV parse both happen in Rust with the
Python GIL released, so a fetch no longer blocks other Python threads, and the
parse avoids the overhead of going through Python's polars bindings twice
(once for the raw text, once for the frame).
What didn't change: if you're upgrading from 0.1.x, this is a drop-in
replacement — same function signatures, same return types, same exceptions.
Nothing in your calling code needs to change.
Requirements
- Python ≥ 3.9
- The spreadsheet must be set to Anyone with the link can view
Installation
pip install scan-google-sheet
# or
uv add scan-google-sheet
Prebuilt wheels are published for common platforms/Python versions; pip falls back to building from source (requires a Rust toolchain) if no matching wheel is available.
Quick start
from scan_google_sheet import read_google_sheet, scan_google_sheet
Eager — returns a DataFrame immediately:
df = read_google_sheet("Sheet1", sheet_id="1BxiMVs0XRA5nFMdKvBdBZjgm...")
Lazy — returns a LazyFrame, participates in Polars query optimisation:
lf = scan_google_sheet("Sheet1", sheet_id="1BxiMVs0XRA5nFMdKvBdBZjgm...")
df = (
lf
.filter(pl.col("year") == 2025)
.select("vessel", "amount")
.collect()
)
You can also pass a full Google Sheets URL instead of a bare sheet ID:
df = read_google_sheet(
"Sheet1",
url="https://docs.google.com/spreadsheets/d/1BxiMVs0.../edit#gid=0",
)
API
read_google_sheet
def read_google_sheet(
sheet_name: str,
sheet_id: str | None = None,
url: str | None = None,
*,
timeout: int = 10,
parse_dates: bool = True,
) -> pl.DataFrame
Fetches the sheet and returns a collected DataFrame. The HTTP fetch and CSV
parse both run in Rust, released from the Python GIL.
scan_google_sheet
def scan_google_sheet(
sheet_name: str,
sheet_id: str | None = None,
url: str | None = None,
*,
timeout: int = 10,
parse_dates: bool = True,
batch_size: int = 1_000,
) -> pl.LazyFrame
Returns a LazyFrame registered via the Polars IO plugin API. Projection
pushdown, predicate pushdown, head(), and streaming are all supported.
Note: Google Sheets does not support partial HTTP reads. The full sheet is always downloaded in one request by the Rust core. Pushdowns reduce processing cost, not network cost. This is the one part of the API that stays in Python:
register_io_sourceis a Polars Python-only hook with no Rust-side equivalent, soscan_google_sheetis a thin Python wrapper around the Rust-backedread_google_sheet.
Parameters shared by both functions:
| Parameter | Type | Default | Description |
|---|---|---|---|
sheet_name |
str |
— | Tab name as shown in Google Sheets |
sheet_id |
str | None |
None |
Spreadsheet ID from the URL |
url |
str | None |
None |
Full Google Sheets URL (ID extracted automatically) |
timeout |
int |
10 |
HTTP timeout in seconds |
parse_dates |
bool |
True |
Attempt automatic date/datetime parsing |
Provide either sheet_id or url, not both.
URL utilities
from scan_google_sheet import extract_sheet_id, build_gviz_url, from_url
# Extract the sheet ID from any Google Sheets URL
sheet_id = extract_sheet_id("https://docs.google.com/spreadsheets/d/ABC123/edit")
# "ABC123"
# Build a gviz CSV export URL from a sheet ID and tab name
url = build_gviz_url("ABC123", "Sheet1")
# "https://docs.google.com/spreadsheets/d/ABC123/gviz/tq?tqx=out:csv&sheet=Sheet1"
# Build a gviz URL directly from a full Google Sheets URL
url = from_url("https://docs.google.com/spreadsheets/d/ABC123/edit", "Sheet1")
Error handling
All exceptions inherit from ReadSheetError, so you can catch everything with
one handler or branch on specific types:
from scan_google_sheet import (
read_google_sheet,
ReadSheetError,
SheetFetchError,
SheetURLError,
SheetParseError,
NetworkError,
ConfigurationError,
)
try:
df = read_google_sheet("Sheet1", sheet_id="...")
except ReadSheetError as e:
match e:
case SheetFetchError() if e.is_auth_error:
print("Make the sheet public (Share → Anyone with the link)")
case SheetFetchError() if e.is_not_found:
print(f"Sheet not found — check the ID: {e.url}")
case NetworkError():
print(f"No connection: {e.cause}")
case SheetURLError(raw=r):
print(f"Could not parse URL: {r!r}")
case SheetParseError():
print(f"CSV parse failed: {e.cause}")
case ConfigurationError():
print(str(e))
Exception hierarchy
ReadSheetError
├── SheetURLError malformed URL or unextractable sheet ID (.raw)
├── SheetFetchError non-200 HTTP response (.url, .status_code)
│ (.is_auth_error, .is_not_found)
├── SheetParseError CSV or Polars parsing failure (.column)
├── NetworkError transport failure, no response received (.url)
└── ConfigurationError invalid argument combination
Raised from Rust via PyO3, but the hierarchy, messages, and
attributes are the same ones the pure-Python 0.1.x release raised.
Making your sheet public
In Google Sheets: Share → Change to Anyone with the link → Viewer → Done.
The export URL used by this library (gviz/tq?tqx=out:csv) requires the sheet
to be publicly readable. No data is ever written.
How it works
Google Sheets URL / ID
│
▼
build_gviz_url() constructs the CSV export URL [Rust]
│
▼
fetch_raw() reqwest GET, GIL released [Rust]
│
▼
parse_csv() polars CSV → DataFrame [Rust]
│
▼
read_google_sheet() PyO3 boundary → pl.DataFrame [Rust → Python]
│
▼
scan_google_sheet() register_io_source() lazy wrapper [Python]
│
▼
LazyFrame / DataFrame ready for your pipeline
The crate also builds as a standalone Rust library (cargo build, no Python
required) — see src/lib.rs for the read_public_sheet entry point used by
the python feature's PyO3 bindings.
Development
git clone https://github.com/Attica-oss/scan_google_sheet
cd scan_google_sheet
# Rust-only build/check
cargo build
cargo test
# Python extension, editable install into a venv
uv venv
source .venv/bin/activate
uv pip install maturin
maturin develop --features python
Changelog
0.2.0
- Full rewrite of the fetch/parse core in Rust (PyO3 + maturin), GIL released during network and parse work
- Public API and exception hierarchy unchanged from
0.1.x scan_google_sheet's lazy IO-plugin wrapper stays in Python (Polarsregister_io_sourcehas no Rust-side equivalent)
0.1.1 and earlier
- Pure-Python implementation (
httpx+polars) read_google_sheetandscan_google_sheet- Polars IO plugin for lazy evaluation
- Structured exception hierarchy
License
MIT © 2026 Garry (Attica-oss)
Metadata
Release files for scan-google-sheet 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| scan_google_sheet-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.13 | CPython 3.13 | Linux glibc 2.17+ x86-64 | Details |
Release files / scan_google_sheet-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | scan_google_sheet-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 7.7 MB |
| Tags | CPython 3.13 Linux glibc 2.17+ x86-64 |
|
SHA-256 checksum How to use checksums |
f3eb107d377c89893068e31f87d5c88defbcfe9e6429ae675bf6a6f6e22b247a
|
|
BLAKE2b-256 checksum How to use checksums |
e7e876b3f2d61754e60693a7fcf64c2df4b4913ceddffb49f55b9f7b7c92cb27
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|