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.

Data Engineerpythonpandasparquet

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

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

Convert CSV to Parquet With PyArrowData projectQuery Parquet Files With DuckDBData projectBack Up Postgres to AWS S3Data projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

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 →