pyarrow-bigquery
An extension library to write to and read from BigQuery tables as PyArrow tables.
Table of Contents
- Installation
- Source Code
- Quick Start
- Examples
- API Reference
- Authentication
Installation
pip install pyarrow-bigquery
Authenticate with Google Cloud (Application Default Credentials), for example:
gcloud auth application-default login
Source Code
https://github.com/xando/pyarrow-bigquery/
Quick Start
Import the namespace package:
import pyarrow as pa
import pyarrow.bigquery as bq
Table and query locations use project.dataset.table. The project in the path is also used as the billing project unless you pass project= explicitly.
Reading
| Goal | API |
|---|---|
| Whole table in memory | read_table |
| Whole query result in memory | read_query |
| Stream a large table in chunks | reader (context manager) |
| Stream a large query in chunks | reader_query (context manager) |
Small table — load entirely
table = bq.read_table("my_project.my_dataset.events")
print(table.num_rows, table.schema)
Small query — load entirely
table = bq.read_query(
project="my_project",
query="SELECT id, ts FROM `my_project.my_dataset.events` WHERE ts >= '2024-01-01'",
)
Large table — iterate batches
with bq.reader("my_project.my_dataset.events", batch_size=10_000) as r:
for chunk in r:
process(chunk) # each chunk is a pa.Table
Large query — iterate batches
with bq.reader_query(
project="my_project",
query="SELECT * FROM `my_project.my_dataset.events`",
batch_size=10_000,
) as r:
print(r.schema)
for chunk in r:
process(chunk)
Writing
| Goal | API |
|---|---|
| Upload a table in one call | write_table |
| Stream many chunks (generator, ETL, etc.) | writer (context manager) |
One-shot upload
table = pa.table({"id": [1, 2, 3], "name": ["a", "b", "c"]})
bq.write_table(table, "my_project.my_dataset.names")
Streaming upload
schema = pa.schema([("id", pa.int64()), ("payload", pa.string())])
with bq.writer(schema, "my_project.my_dataset.streamed") as w:
for batch in generate_batches():
w.write_table(batch) # or w.write_batch(record_batch)
Examples
Column projection and row filters
BigQuery applies columns and row_restrictions before data is streamed to clients:
table = bq.read_table(
"my_project.my_dataset.events",
columns=["user_id", "event_name", "ts"],
row_restrictions="event_name = 'purchase' AND ts >= '2024-06-01'",
)
The same options work on reader, read_query, and reader_query.
When row_restrictions (or the table itself) matches no rows, read_table and
read_query return a zero-row pa.Table that still carries the BigQuery
schema, so downstream code can rely on the columns being present.
Casting columns on read
Use cast_column to convert selected top-level columns to PyArrow types:
table = bq.read_table(
"my_project.my_dataset.events",
cast_column={"field_name": pa.int32(), "other_field": pa.list_(pa.float64())},
)
Use cast_type to convert every top-level column matching an original PyArrow
type:
table = bq.read_table(
"my_project.my_dataset.events",
cast_type={pa.int64(): pa.int32(), pa.list_(pa.int64()): pa.list_(pa.float64())},
cast_column={"large_id": pa.int64()}, # override the type rule for this column
)
Type rules match the original type of the whole column and apply once. They do
not recursively match nested fields: pa.int64(): pa.int32() leaves lists of
integers unchanged unless a list type is also mapped. Unmatched rules are
ignored. When both options match a column, cast_column takes precedence;
conversion goes directly from the original type to the chosen target.
Both options work on reader, read_query, and reader_query, with both read
engines. Casts run locally on each chunk using PyArrow's safe casting.
Unmatched columns keep their types; column order, nulls, and schema metadata are
preserved. Unsupported conversions, overflow, and precision loss raise an Arrow
error. A named column absent from the read schema (including one excluded by
columns) raises KeyError. Nested types such as lists can be converted by
specifying the target type of the whole column.
reader.schema reflects the requested types, and empty results retain that
schema as well.
Query location and large results
Pass location when the query must run in a specific region. For very large query outputs, materialize into a temporary table and read via the Storage API:
with bq.reader_query(
project="my_project",
query="SELECT * FROM huge_join ...",
location="EU",
large_results=True,
batch_size=50_000,
) as r:
for chunk in r:
process(chunk)
# temporary result table is deleted on exit when large_results=True
read_query(..., large_results=True) works the same way but loads everything into memory.
Parallel workers and IPC exchange
Reads and writes use a pool of threads or processes (worker_count, worker_type). For process workers, pass an IPC exchange compatible with multiprocessing (default: exchange.ArrowIpc()):
import multiprocessing
import pyarrow.bigquery.exchange as exchange
with bq.reader(
"my_project.my_dataset.big_table",
worker_type=multiprocessing.Process,
worker_count=8,
ipc_exchange=exchange.ArrowIpc(),
compression="zstd",
) as r:
for chunk in r:
process(chunk)
Thread workers default to exchange.Memory(). Other exchanges (Feather, SharedMemory, …) are available under pyarrow.bigquery.exchange for advanced tuning.
worker_type / ipc_exchange only apply to the Python read engine (the default). See below for the Rust engine, which manages concurrency internally.
Read engines (engine="python" vs "rust")
The read APIs (read_table, read_query, reader, reader_query) accept an engine parameter selecting which implementation runs the BigQuery Storage Read session.
engine |
Default | How it works |
|---|---|---|
"python" |
✅ | Spawns a worker pool (threads or processes per worker_type) using the google-cloud-bigquery-storage Python client; results travel through a configurable ipc_exchange. Behaviour is identical to 0.6.x. |
"rust" |
Uses the bundled pyarrow.bigquery._rust extension: a native tonic gRPC client + tokio runtime decodes Arrow IPC and hands batches to Python over the Arrow C Data Interface (no IPC roundtrip, no IPC exchange to configure). |
# Default — Python worker pool, identical to 0.6.x
table = bq.read_table("my_project.my_dataset.events")
# Opt-in Rust engine
table = bq.read_table("my_project.my_dataset.events", engine="rust")
# Works the same way for the streaming context manager
with bq.reader("my_project.my_dataset.events", engine="rust", batch_size=10_000) as r:
for chunk in r:
process(chunk)
With engine="rust":
columns,row_restrictions,batch_size,compression(None | "lz4" | "zstd"),project,delete_source_on_exitall work the same as in the Python engine.worker_countis honoured: it caps the number of read streams the session opens (max_stream_count = worker_count * 3, same default as the Python path).worker_typeandipc_exchangeare ignored (a warning is emitted if you pass non-default values). The Rust path always uses a single shared tokio runtime; there is no thread/process choice to make and no IPC exchange to serialize through.- Authentication uses the same Application Default Credentials chain as the Python client (
gcp_authcrate under the hood).
Choosing an engine: the default is "python" because it's the well-tested, in-place implementation. Switch to "rust" if you've measured a benefit on your workload and are happy depending on the prebuilt wheel for your platform. Both engines pass the same unit-test suite and return the same row counts.
Write engines (engine="python" vs "rust")
The write APIs accept the same engine selector as reads. The default
engine="python" keeps the existing Python workers and protobuf upload path.
engine="rust" uses the bundled extension to import Arrow batches directly,
encode Arrow IPC, and manage the Storage Write API connections:
bq.write_table(table, "my_project.my_dataset.events", engine="rust")
with bq.writer(table.schema, "my_project.my_dataset.events",
engine="rust", table_create=False, worker_count=4) as w:
w.write_table(table)
# w.write_batch(record_batch) also works.
project, table_create, table_expire, table_overwrite, and batch_size
retain their existing meanings. worker_count limits the native write streams;
worker_type is ignored by Rust, with a warning for non-default values. The Rust
path uses bounded queues and does not create temporary Feather files or Python
worker threads/processes.
Each Rust stream pipelines up to four unacknowledged append requests. Requests
and worker queues remain bounded; all acknowledgements and the final stream
status are checked before committing. Increase worker_count to use more
streams when your workload benefits from additional concurrency.
For bulk uploads, set batch_size explicitly (write_table defaults to 10 rows).
For example, start with the following and measure worker counts on your workload:
bq.write_table(
table, "my_project.my_dataset.events",
engine="rust", compression="zstd",
worker_count=8, batch_size=50_000,
)
Both engines support compression="gzip" to compress outgoing gRPC requests:
bq.write_table(table, "my_project.my_dataset.events", compression="gzip")
with bq.writer(table.schema, "my_project.my_dataset.events",
engine="rust", compression="gzip", table_create=False) as w:
w.write_table(table)
The Rust writer also supports compression="lz4" and compression="zstd"
using Arrow buffer compression, matching the read APIs:
bq.write_table(table, "my_project.my_dataset.events", engine="rust",
compression="zstd", worker_count=8, batch_size=50_000)
table_back = bq.read_table("my_project.my_dataset.events", engine="rust",
compression="zstd")
| Engine | Read compression | Write compression |
|---|---|---|
"python" |
None, "lz4", "zstd" |
None, "gzip" |
"rust" |
None, "lz4", "zstd" |
None, "lz4", "zstd", "gzip" |
Compression defaults to None (disabled). LZ4/Zstandard compress Arrow buffers;
gzip compresses gRPC requests. Each write selects one compression layer. The
Python writer sends protobuf rows and rejects LZ4/Zstandard before creating a
table. Rust checks the complete request size after Arrow encoding and before
gRPC compression, including the schema on the first request.
Rust writes use pending streams. A successful context exit waits for all appends, finalizes the streams, and commits them together. A write failure or exception in the context prevents that commit. Table creation and overwrite happen separately and are not rolled back. As with other remote commits, an error receiving the commit response can leave its outcome uncertain.
Append connection failures are reported to the caller. Automatic reconnect and replay of outstanding Rust append requests are not implemented.
The Rust engine validates schema support before creating or overwriting a table.
It normalizes timestamps to UTC microseconds (naive timestamps are interpreted as
UTC), whole-day Date64 values to Date32, and null lists to empty lists. Null
records inside lists are omitted; null scalar list elements are rejected.
Decimal128 requires at most nine fractional digits and 29 integer digits and is
normalized to NUMERIC. Conversions that would lose precision or overflow raise
an error. Time-of-day arrays, Decimal256, dictionary arrays, and direct nested
lists are unsupported; use engine="python" where it supports the input, or
convert to a supported type. Batches are split to fit the
Storage Write API request limit; a single oversized row raises an error.
Table creation options on write
# Replace table if it already exists
bq.write_table(
table,
"my_project.my_dataset.snapshot",
table_overwrite=True,
)
# Auto-expire after 7 days
bq.write_table(
table,
"my_project.my_dataset.temp_export",
table_expire=7 * 24 * 3600,
)
# Append to an existing table (must already exist and match schema)
bq.write_table(
more_rows,
"my_project.my_dataset.events",
table_create=False,
)
Chunked write with batch_size
write_table splits the input into upload chunks (default batch_size=10 rows per chunk):
bq.write_table(large_table, "my_project.my_dataset.loaded", batch_size=5000)
API Reference
Writing
pyarrow.bigquery.write_table
Writes a PyArrow table to BigQuery. Returns nothing.
Parameters:
table:pa.Table— must be non-empty.where:str— destinationproject.dataset.table.project:str | None, defaultNone— billing project; inferred fromwherewhen omitted.table_create:bool, defaultTrue— create the destination table if missing.table_expire:int | None, defaultNone— seconds until table expiry (only when creating).table_overwrite:bool, defaultFalse— delete and recreate the table if it exists.worker_type:threading.Thread | multiprocessing.Process, defaultthreading.Thread.worker_count:int, defaultos.cpu_count().batch_size:int, default10— rows per upload chunk inside the call.compression:None | "gzip" | "lz4" | "zstd", defaultNone— gzip compresses gRPC requests with either engine; LZ4/Zstandard compress Arrow buffers with Rust.engine:"python" | "rust", default"python"— implementation selector; see Write engines.
bq.write_table(table, "my_project.my_dataset.out")
pyarrow.bigquery.writer (context manager)
Incremental writes. Constructor: writer(schema, where, **options).
Parameters: same table-creation, worker, compression, and engine options as write_table
(no batch_size on the context manager itself).
Methods:
write_table(table: pa.Table)— enqueue a table chunk.write_batch(batch: pa.RecordBatch)— enqueue a single record batch.
schema = pa.schema([("n", pa.int64())])
with bq.writer(schema, "my_project.my_dataset.incremental") as w:
w.write_batch(pa.record_batch([pa.array([1, 2])], schema=schema))
w.write_table(pa.table({"n": [3, 4, 5]}))
pyarrow.bigquery.writer.write_table / write_batch
See writer above. Typical loop:
with bq.writer(schema, "my_project.my_dataset.rows") as w:
for i in range(1000):
w.write_table(pa.table({"value": [i] * 10}, schema=schema))
Reading
pyarrow.bigquery.read_table
Loads a full table into memory (pa.concat_tables over internal batches).
Parameters:
source:str—project.dataset.table.project:str | None, defaultNone.columns:list[str] | None, defaultNone— subset of columns; all columns when omitted.cast_column:Mapping[str, pa.DataType] | None, defaultNone— safe casts for named top-level columns; see Casting columns on read.cast_type:Mapping[pa.DataType, pa.DataType] | None, defaultNone— safe casts for all top-level columns with a matching original type;cast_columnoverrides these rules.row_restrictions:str | None, defaultNone— SQL filter pushed down to BigQuery Storage Read.worker_type,worker_count— parallel fetch backend (Python engine only;worker_countstill caps stream count under the Rust engine).ipc_exchange: exchange instance for worker handoff; defaultMemory(threads) orArrowIpc(processes). Ignored whenengine="rust".batch_size:int, default100— target rows per internal chunk.compression:None | "lz4" | "zstd", defaultNone— Storage API Arrow compression.engine:"python" | "rust", default"python"— implementation selector; see Read engines.
table = bq.read_table("my_project.my_dataset.events", columns=["id"], batch_size=500)
pyarrow.bigquery.read_query
Runs a query and returns the full result as one pa.Table. Accepts the same read tuning parameters as read_table (including engine), plus:
location:str | None— query job location.large_results:bool, defaultFalse— materialize to a temp table, then read (deleted on exit).large_results_dataset:str, default"_temp_pyarrow_bigquery".large_results_expiration_ms:int, default86400000— dataset default TTL when the dataset is first created.
table = bq.read_query(
project="my_project",
query="SELECT id FROM `my_project.my_dataset.events` LIMIT 1000",
location="US",
)
pyarrow.bigquery.reader (context manager)
Streams a table as an iterator of pa.Table chunks.
Parameters: same as read_table (including engine).
Attributes:
schema:pa.Schema— available after__enter__.
parts = []
with bq.reader("my_project.my_dataset.events", batch_size=2000) as r:
print(r.schema)
for chunk in r:
parts.append(chunk)
full = pa.concat_tables(parts) if parts else pa.table({})
pyarrow.bigquery.reader_query (context manager)
Runs a query, then streams the result like reader. Query-related parameters match read_query (including engine).
with bq.reader_query(
project="my_project",
query="SELECT * FROM `my_project.my_dataset.events`",
large_results=True,
) as r:
for chunk in r:
print(chunk.num_rows)
IPC exchange (pyarrow.bigquery.exchange)
Workers pass Arrow tables through a pluggable exchange. Implementations set thread_compatible / process_compatible flags:
| Class | Threads | Processes | Notes |
|---|---|---|---|
Memory |
yes | no | Default for thread workers |
ArrowIpc |
no | yes | Default for process workers; mmap temp files |
Feather |
no | yes | Feather temp files |
SharedMemory |
no | yes | POSIX shared memory |
SharedMemoryDict |
yes | yes | Slow; mainly for completeness |
import pyarrow.bigquery.exchange as exchange
with bq.reader(
"my_project.my_dataset.t",
worker_type=multiprocessing.Process,
ipc_exchange=exchange.SharedMemory(),
) as r:
...
Authentication
The library uses Application Default Credentials. Ensure the active principal can read/write the target datasets (BigQuery and BigQuery Storage API permissions).
Metadata
Release files for pyarrow-bigquery 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyarrow_bigquery-0.10.0.tar.gz | 97.4 kB | Without provenance | Details for pyarrow_bigquery-0.10.0.tar.gz |
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| pyarrow_bigquery-0.10.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details for pyarrow_bigquery-0.10.0-cp310-abi3-win_amd64.whl |
| pyarrow_bigquery-0.10.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details for pyarrow_bigquery-0.10.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
| pyarrow_bigquery-0.10.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details for pyarrow_bigquery-0.10.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl |
| pyarrow_bigquery-0.10.0-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details for pyarrow_bigquery-0.10.0-cp310-abi3-macosx_11_0_arm64.whl |
| pyarrow_bigquery-0.10.0-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details for pyarrow_bigquery-0.10.0-cp310-abi3-macosx_10_12_x86_64.whl |
Total release size: 22.1 MB
Release files / pyarrow_bigquery-0.10.0.tar.gz
| Download URL | pyarrow_bigquery-0.10.0.tar.gz |
|---|---|
| Size | 97.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dfcb14412fbec6274d14b19965d017a198d002ce71aef85b06a54d04752c2f87
|
|
BLAKE2b-256 checksum How to use checksums |
2e48783448b2b3399c91c896fd65418ddc455dea6470a70894db9de424b83428
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Release files / pyarrow_bigquery-0.10.0-cp310-abi3-win_amd64.whl
| Download URL | pyarrow_bigquery-0.10.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 4.4 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
f6189077329018f1424fcad16090ec4523963aca0bb71e7228c129b7b40862d4
|
|
BLAKE2b-256 checksum How to use checksums |
4e01d7d1631802c86ada87127d001be39443318cd32ab70d84f03ed1c9349cbd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Release files / pyarrow_bigquery-0.10.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
| Download URL | pyarrow_bigquery-0.10.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
|---|---|
| Size | 4.7 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
be05e750a07d691a97d9e562f0016cb90e44476218a73cc00c9fa348039b8c13
|
|
BLAKE2b-256 checksum How to use checksums |
97d3132650a6f14c62fe18cccc36170b5219464042bf9ba8ab54adcd8492b7bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Release files / pyarrow_bigquery-0.10.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
| Download URL | pyarrow_bigquery-0.10.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl |
|---|---|
| Size | 4.5 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
a592e69b9c11894b7b87074cc412a42e3e525d96da76cbb451651c866732bed5
|
|
BLAKE2b-256 checksum How to use checksums |
b250e8c1c282c2c2a839ce9c272f1ba718bfb064a6897dc81d9adb17eb85db97
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Release files / pyarrow_bigquery-0.10.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | pyarrow_bigquery-0.10.0-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 4.1 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
09ee3177a422c6f33158c78ed9d459037485f3d1d8b27408a79061d3abeebf6d
|
|
BLAKE2b-256 checksum How to use checksums |
dc40c66ad9d811a674f97efc0c2ee55832c271701e3564d43bbbae8312c9159f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Release files / pyarrow_bigquery-0.10.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | pyarrow_bigquery-0.10.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 4.4 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
c9169f87bc633d5c7a3664c26892e04eeefac246d5cafb82d5389e4a5da9c47c
|
|
BLAKE2b-256 checksum How to use checksums |
77e73a28e5d025dbe4f86e959efd87e5fab8db3df3be49d623d86c1dbc0bbe32
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Security for pyarrow-bigquery 0.10.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.