S3FS¶
S3FSCursor¶
S3FSCursor is a lightweight cursor that directly handles the CSV file of the query execution result output to S3. Unlike ArrowCursor or PandasCursor, this cursor does not require pandas or pyarrow dependencies, making it ideal for environments where installing these libraries is not desirable.
Key features:
No pandas or pyarrow dependencies required
Lightweight CSV parsing (custom parser or Python’s built-in
csvmodule)Lower memory footprint for simple query results
Full DB API 2.0 compatibility
You can use the S3FSCursor by specifying the cursor_class
with the connect method or connection object.
from pyathena import connect
from pyathena.s3fs.cursor import S3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=S3FSCursor).cursor()
from pyathena.connection import Connection
from pyathena.s3fs.cursor import S3FSCursor
cursor = Connection(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=S3FSCursor).cursor()
It can also be used by specifying the cursor class when calling the connection object’s cursor method.
from pyathena import connect
from pyathena.s3fs.cursor import S3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2").cursor(S3FSCursor)
from pyathena.connection import Connection
from pyathena.s3fs.cursor import S3FSCursor
cursor = Connection(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2").cursor(S3FSCursor)
Support fetch and iterate query results.
from pyathena import connect
from pyathena.s3fs.cursor import S3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=S3FSCursor).cursor()
cursor.execute("SELECT * FROM many_rows")
print(cursor.fetchone())
print(cursor.fetchmany())
print(cursor.fetchall())
from pyathena import connect
from pyathena.s3fs.cursor import S3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=S3FSCursor).cursor()
cursor.execute("SELECT * FROM many_rows")
for row in cursor:
print(row)
Execution information of the query can also be retrieved.
from pyathena import connect
from pyathena.s3fs.cursor import S3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=S3FSCursor).cursor()
cursor.execute("SELECT * FROM many_rows")
print(cursor.state)
print(cursor.state_change_reason)
print(cursor.completion_date_time)
print(cursor.submission_date_time)
print(cursor.data_scanned_in_bytes)
print(cursor.engine_execution_time_in_millis)
print(cursor.query_queue_time_in_millis)
print(cursor.total_execution_time_in_millis)
print(cursor.query_planning_time_in_millis)
print(cursor.service_processing_time_in_millis)
print(cursor.output_location)
Type conversion¶
S3FSCursor converts Athena data types to Python types using the built-in converter. The following type mappings are used:
Athena Type |
Python Type |
|---|---|
boolean |
bool |
tinyint, smallint, integer, bigint |
int |
float, double, real |
float |
decimal |
decimal.Decimal |
char, varchar, string |
str |
date |
datetime.date |
timestamp |
datetime.datetime |
timestamp with time zone |
datetime.datetime (timezone-aware) |
time |
datetime.time |
time with time zone |
datetime.time (timezone-aware) |
varbinary |
bytes |
array, map, row (struct) |
Parsed into Python list/dict (see Type hints for complex types for the types of nested values); values too complex to parse are returned as the original string |
json |
Parsed JSON value (dict, list, or scalar) |
If you want to customize type conversion, create a converter class like this:
from typing import Any
from pyathena.s3fs.converter import DefaultS3FSTypeConverter
class CustomS3FSTypeConverter(DefaultS3FSTypeConverter):
def __init__(self) -> None:
super().__init__()
# Override specific type mappings
self._mappings["custom_type"] = self._convert_custom
def _convert_custom(self, value: str) -> Any:
# Your custom conversion logic
return value.upper()
Then specify an instance of this class in the converter argument when creating a cursor.
from pyathena import connect
from pyathena.s3fs.cursor import S3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2").cursor(S3FSCursor, converter=CustomS3FSTypeConverter())
CSV reader options¶
S3FSCursor supports pluggable CSV reader implementations to control how NULL values and empty strings are handled. Two readers are provided:
AthenaCSVReader(default): Custom parser that distinguishes between NULL and empty stringEmptyStringAsNullCSVReader: Uses Python’s built-incsvmodule; both NULL and empty string becomeNonein cursor results
Default behavior (AthenaCSVReader):
By default, AthenaCSVReader is used, which correctly distinguishes between NULL
values and empty strings in query results.
from pyathena import connect
from pyathena.s3fs.cursor import S3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=S3FSCursor).cursor()
cursor.execute("SELECT NULL AS null_col, '' AS empty_col")
row = cursor.fetchone()
print(row) # (None, '') - NULL is None, empty string is ''
Treating empty strings as NULL:
Use EmptyStringAsNullCSVReader when empty strings should be treated as NULL.
It parses result files with Python’s csv module and returns None for both NULL and quoted empty fields.
from pyathena import connect
from pyathena.s3fs.cursor import S3FSCursor
from pyathena.s3fs.reader import EmptyStringAsNullCSVReader
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=S3FSCursor,
cursor_kwargs={"csv_reader": EmptyStringAsNullCSVReader}).cursor()
cursor.execute("SELECT NULL AS null_col, '' AS empty_col")
row = cursor.fetchone()
print(row) # (None, None) - Both NULL and empty string become None
Comparison of CSV readers:
Reader |
Implementation |
NULL value |
Empty string |
|---|---|---|---|
AthenaCSVReader (default) |
Custom parser |
None |
‘’ (empty string) |
EmptyStringAsNullCSVReader |
Python csv module |
None |
None |
Why the difference?
Athena’s CSV output format distinguishes between NULL values and empty strings:
NULL: unquoted empty field (e.g.,
a,,b-> the middle field is NULL)Empty string: quoted empty field (e.g.,
a,"",b-> the middle field is an empty string)
Python’s standard csv module parses both cases as empty strings, losing this distinction.
The AthenaCSVReader implements a custom parser that preserves the difference.
CSV reader rename in PyAthena 4.0¶
PyAthena 4.0 renames DefaultCSVReader to EmptyStringAsNullCSVReader and removes the old name.
Update imports and csv_reader arguments to use the new name:
from pyathena.s3fs.reader import EmptyStringAsNullCSVReader
cursor = connection.cursor(S3FSCursor, csv_reader=EmptyStringAsNullCSVReader)
AthenaCSVReader remains the default.
Iterating over EmptyStringAsNullCSVReader directly now returns None for empty fields instead of empty strings.
Cursor results are unchanged.
Custom CSV readers¶
Custom reader classes must satisfy the pyathena.s3fs.reader.CSVReader protocol; inheriting from it is optional.
Pass the class through csv_reader in the cursor constructor or an individual execute() call.
Accept a text stream as
file_objand adelimiterkeyword argument in the constructor.Implement
__iter__()and__next__()to yield sequences of strings orNone, and raiseStopIterationat the end of the stream. The cursor converts each value by its column type and treatsNoneas NULL.Implement
close()to close the underlying stream.
The reader is used only for result files; managed query results are read through the Athena API.
S3 read options¶
S3FSCursor reads a CSV result file in 32 MiB blocks with the "background" fsspec cache, which reads the next block while rows are converted and keeps two blocks.
With block_size given to execute(), the file is read in blocks of that size with the "bytes" cache.
A filesystem_class other than PyAthena’s S3FileSystem and AioS3FileSystem, including a subclass, reads the file with its own default cache type.
Limitations¶
S3FSCursor has some limitations compared to ArrowCursor or PandasCursor:
No UNLOAD support: S3FSCursor reads CSV results directly and does not support the UNLOAD option that outputs results in Parquet format.
Sequential reading: Results are read row by row from the CSV file, which may be slower for very large result sets compared to columnar formats.
No DataFrame conversion: There is no
as_pandas()oras_arrow()method. Use PandasCursor or ArrowCursor if you need DataFrame operations.
When to use S3FSCursor¶
S3FSCursor is recommended when:
You want to minimize dependencies (no pandas/pyarrow required)
You’re working in a constrained environment (e.g., AWS Lambda with size limits)
You only need simple row-by-row result processing
Memory efficiency is important and results don’t need columnar operations
For large-scale data processing or analytical workloads, consider using ArrowCursor or PandasCursor instead.
AsyncS3FSCursor¶
AsyncS3FSCursor is an AsyncCursor that uses the same lightweight CSV parsing as S3FSCursor. This cursor is useful when you need to execute queries asynchronously without pandas or pyarrow dependencies.
You can use the AsyncS3FSCursor by specifying the cursor_class
with the connect method or connection object.
from pyathena import connect
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=AsyncS3FSCursor).cursor()
from pyathena.connection import Connection
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = Connection(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=AsyncS3FSCursor).cursor()
It can also be used by specifying the cursor class when calling the connection object’s cursor method.
from pyathena import connect
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2").cursor(AsyncS3FSCursor)
from pyathena.connection import Connection
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = Connection(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2").cursor(AsyncS3FSCursor)
The default number of workers is 5 or cpu number * 5. If you want to change the number of workers you can specify like the following.
from pyathena import connect
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=AsyncS3FSCursor).cursor(max_workers=10)
The execute method of the AsyncS3FSCursor returns the tuple of the query ID and the future object.
from pyathena import connect
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=AsyncS3FSCursor).cursor()
query_id, future = cursor.execute("SELECT * FROM many_rows")
The return value of the future object is an AthenaS3FSResultSet object.
This object has an interface similar to AthenaResultSet.
from pyathena import connect
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=AsyncS3FSCursor).cursor()
query_id, future = cursor.execute("SELECT * FROM many_rows")
result_set = future.result()
print(result_set.state)
print(result_set.state_change_reason)
print(result_set.completion_date_time)
print(result_set.submission_date_time)
print(result_set.data_scanned_in_bytes)
print(result_set.engine_execution_time_in_millis)
print(result_set.query_queue_time_in_millis)
print(result_set.total_execution_time_in_millis)
print(result_set.query_planning_time_in_millis)
print(result_set.service_processing_time_in_millis)
print(result_set.output_location)
print(result_set.description)
for row in result_set:
print(row)
from pyathena import connect
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=AsyncS3FSCursor).cursor()
query_id, future = cursor.execute("SELECT * FROM many_rows")
result_set = future.result()
print(result_set.fetchall())
As with AsyncCursor, you need a query ID to cancel a query.
from pyathena import connect
from pyathena.s3fs.async_cursor import AsyncS3FSCursor
cursor = connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2",
cursor_class=AsyncS3FSCursor).cursor()
query_id, future = cursor.execute("SELECT * FROM many_rows")
cursor.cancel(query_id)
AioS3FSCursor¶
AioS3FSCursor is a native asyncio cursor that uses the same lightweight CSV parsing as S3FSCursor.
Unlike AsyncS3FSCursor which uses concurrent.futures, this cursor uses
asyncio.to_thread() for both result set creation and fetch operations,
keeping the event loop free.
Since AthenaS3FSResultSet lazily streams rows from S3 via a CSV reader,
fetch methods are async and require await.
from pyathena import aio_connect
from pyathena.aio.s3fs.cursor import AioS3FSCursor
async with await aio_connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2") as conn:
cursor = conn.cursor(AioS3FSCursor)
await cursor.execute("SELECT * FROM many_rows")
print(await cursor.fetchone())
print(await cursor.fetchmany(10))
print(await cursor.fetchall())
Async iteration is supported:
from pyathena import aio_connect
from pyathena.aio.s3fs.cursor import AioS3FSCursor
async with await aio_connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2") as conn:
cursor = conn.cursor(AioS3FSCursor)
await cursor.execute("SELECT * FROM many_rows")
async for row in cursor:
print(row)
Execution information of the query can also be retrieved:
from pyathena import aio_connect
from pyathena.aio.s3fs.cursor import AioS3FSCursor
async with await aio_connect(s3_staging_dir="s3://YOUR_S3_BUCKET/path/to/",
region_name="us-west-2") as conn:
cursor = conn.cursor(AioS3FSCursor)
await cursor.execute("SELECT * FROM many_rows")
print(cursor.state)
print(cursor.data_scanned_in_bytes)
print(cursor.output_location)