How to Write Pandas to Parquet and Partition an S3 Export by Date
A daily job that writes pandas to Parquet at a flat key like s3://lake/orders.parquet gives Athena and Spark nothing to prune - every query scans the whole file. The fix is to compute a Hive-style key, orders/year=YYYY/month=MM/day=DD/orders.parquet, from today's date before the put_object call. Partition pruning then drops scanned data (and the bill) by 99%+ on long-history datasets.
Why a flat Parquet key kills partition pruning
The export serializes a DataFrame with df.to_parquet(buf) and pushes the bytes to
S3. That part is fine - pandas to Parquet is exactly the right format for a data lake.
The problem is the key it writes to:
key = "orders.parquet"
s3.put_object(Bucket="lake", Key=key, Body=buf.getvalue())
A single flat object, overwritten daily. When the analytics team points Athena, Spark,
or Trino at s3://lake/, there is no partition column in the path, so a query like
WHERE year = 2026 AND month = 5 cannot skip anything - it reads every byte of every
file under the prefix. On a dataset with months or years of history, that is the single
most expensive mistake you can make, because Athena bills by data scanned.
The engines all understand one universal layout: Hive-style partitions, where each
partition column is encoded into the directory path as key=value/.
s3://lake/orders/year=2026/month=05/day=24/orders.parquet
s3://lake/orders/year=2026/month=05/day=25/orders.parquet
Now WHERE year = 2026 AND month = 5 touches only the matching directories and prunes
the rest before reading a single row.
Compute the Hive-partitioned key from today's date
The DataFrame is already being serialized correctly - all that changes is the S3 key.
today is a datetime.date computed just above the key. Build the path from its
fields, zero-padding month and day to two digits (the convention every engine expects):
from datetime import date
today = date.today()
key = (
f"orders/year={today.year}/month={today.month:02d}/"
f"day={today.day:02d}/orders.parquet"
)
s3.put_object(Bucket="lake", Key=key, Body=buf.getvalue())
The :02d format spec is what turns month 5 into month=05. Athena registers
partition values as strings, and mixing month=5 with month=05 creates two distinct
partitions - so pad consistently everywhere.
Run the export and confirm the layout:
cd /workspace && python3 export.py
aws s3 ls --recursive s3://lake/
You should see a single object under orders/year=.../month=.../day=.../orders.parquet
and nothing at the bucket root.
Clean up the old flat object
If a previous run wrote s3://lake/orders.parquet at the root, it lingers - S3 keys are
independent, so writing a new partitioned key does not remove the old one. A stray flat
file at the prefix root confuses partition discovery, so delete it:
aws s3 rm s3://lake/orders.parquet
How the pieces fit for readers
The partition values live only in the path, not inside the Parquet file. When DuckDB or
Athena crawls the tree it reconstructs year, month, and day as virtual columns
from the directory names. That is why the whole scheme works with zero extra metadata:
-- DuckDB reads the Hive layout natively
SELECT * FROM 's3://lake/orders/**/*.parquet'
WHERE year = 2026 AND month = 5;
Production exports almost always partition by load date this way, and often by tenant or region on top. Tools like dbt with Iceberg or Delta Lake generate these exact paths for you - the layout you wrote by hand is what those frameworks produce under the hood.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Writing a pandas DataFrame to Parquet and putting it on S3 with boto3
- Building a Hive-style year/month/day partition key with zero-padded fields
- Verifying the S3 layout with aws s3 ls and removing a stale flat object
FAQ
How do I write a pandas DataFrame to Parquet?
Call df.to_parquet(target). Pass a file path to write to disk, or a BytesIO buffer to keep the bytes in memory - for example df.to_parquet(buf, index=False), then upload buf.getvalue() to S3. Pandas uses PyArrow under the hood and defaults to snappy compression.
How do I partition an S3 Parquet export by date?
Encode the date into the S3 key as Hive-style partition columns instead of writing a flat file: orders/year=YYYY/month=MM/day=DD/orders.parquet. Compute it from today's date with zero-padded month and day, then put_object to that key. Athena, Spark, and Trino then prune by date automatically.
Why does Athena scan all my data even with a WHERE filter?
A WHERE filter only prunes partitions when the filter columns are actual partition columns in the S3 path. If your data sits in one flat file or a prefix with no key=value directories, Athena has nothing to prune and reads everything. Rewrite the layout to Hive-style partitions to fix it.
What is Hive-style partitioning?
Hive-style partitioning encodes each partition column into the directory path as key=value, such as year=2026/month=05/day=24/. Every major query engine (Athena, Spark, Trino, DuckDB) reads this layout natively and reconstructs the partition values as virtual columns from the path.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →