polars cheat sheet
Expressions, lazy scans, group by, joins, and the streaming engine that let one machine handle what used to need a cluster.
Reading and scanning
pl.scan_parquet("s3://lake/orders/**/*.parquet")- scan_ builds a LazyFrame and pushes projections and filters down into the reader, while read_ pulls everything into memory first.
pl.read_csv("orders.csv", schema_overrides={"order_id": pl.Int32}, try_parse_dates=True)- schema_overrides replaced the old dtypes argument in polars 1.0. Type inference only reads the first 100 rows by default.
pl.scan_csv("orders.csv", infer_schema_length=None)- Scans the whole file to infer types. This is the fix for a column that looks like an integer for 100 rows and then hits an alphanumeric id.
pl.read_database_uri(query="select * from orders", uri=dsn)- Uses ConnectorX and returns Arrow buffers directly, skipping the row-by-row DBAPI conversion that dominates a pandas read_sql.
lf.sink_parquet("out.parquet", compression="zstd")- Executes the lazy query straight to disk in batches, so peak memory tracks the batch size rather than the size of the result.
Expressions
df.with_columns( (pl.col("revenue") - pl.col("cost")).alias("margin"), pl.col("region").str.to_uppercase(), )- with_columns takes any number of expressions and evaluates them in parallel. Expressions in one call cannot see each other's output, so chain a second call.
pl.when(pl.col("amount") > 100).then(pl.lit("high")).otherwise(pl.lit("low"))- The polars conditional. Every branch must resolve to the same dtype, so wrap literals in pl.lit rather than relying on inference.
pl.col("amount").sum().over("customer_id")- A window function that keeps row count intact, matching a SQL OVER clause. Add mapping_strategy="join" when the inner expression changes length.
pl.col(pl.String).str.strip_chars()- Expressions can select by dtype, so one call cleans every string column. Combine with pl.exclude to skip key columns you must not touch.
pl.col("payload").str.json_path_match("$.user.id")- Extracts one field without decoding the whole document. Full parsing with str.json_decode should be given an explicit schema to avoid a scan.
Filtering, selection, and sorting
df.filter((pl.col("amount") > 100) & (pl.col("region") == "EU"))- Parentheses are mandatory because & binds tighter than the comparison operators in Python. Passing several arguments to filter ANDs them for you.
df.select(cs.numeric() - cs.by_name("id"))- The polars.selectors module supports set algebra over column groups, which beats hand-maintaining a list of forty metric columns.
df.sort("amount", descending=True, nulls_last=True)- Nulls sort first by default, which quietly puts the empty rows at the top of every worst-offenders report you build.
df.unique(subset=["order_id"], keep="last", maintain_order=True)- Deduplication does not preserve row order unless asked, and maintain_order gives up some parallelism on large frames.
df.head(10).glimpse()- glimpse prints dtypes and leading values column by column, which stays readable in a terminal where a 60-column frame does not.
Group by and aggregation
df.group_by("region").agg( pl.col("amount").sum().alias("total"), pl.col("order_id").n_unique().alias("orders"), )- group_by replaced groupby in polars 1.0. Aggregations are ordinary expressions, so anything valid in select works inside agg.
df.group_by("region", maintain_order=True)- Group order is nondeterministic by default because groups are built in parallel. Only pay for ordering when the output is user-facing.
pl.col("amount").filter(pl.col("status") == "paid").sum()- Filtering inside an aggregation gives conditional sums in a single pass, the polars form of SUM(CASE WHEN ... THEN ... END).
df.group_by_dynamic("ts", every="1h", period="24h", closed="right")- Tumbling and sliding windows over time. The index column must be sorted, and every shorter than period produces deliberately overlapping windows.
pl.col("amount").sort_by("ts").last()- Latest value per group ordered by another column, which removes the usual sort-then-deduplicate pass entirely.
Joins and combining
df.join(dim, on="customer_id", how="left", validate="m:1")- validate raises on unexpected fan-out instead of silently multiplying rows. polars keeps one key column for equi-joins, so pass coalesce=False to see both sides.
df.join_asof(quotes, on="ts", by="symbol", strategy="backward", tolerance="1s")- The point-in-time join. Both frames must be sorted on the asof key, and tolerance takes a duration string rather than a timedelta object.
df.join(other, how="anti", on="order_id")- Anti and semi joins are first class, so "which rows never made it downstream" is one call instead of a join plus an indicator filter.
pl.concat([a, b], how="diagonal_relaxed")- Unions frames with different columns and supercasts mismatched dtypes. Plain vertical concat raises on any schema difference at all.
df.explode("items").unnest("payload")- explode expands a list column to one row per element and unnest flattens a struct into columns. Chaining them is the standard JSON flattening pass.
Lazy execution and streaming
lf.collect(engine="streaming")- Runs the query in batches so it can exceed available memory. This replaced the older collect(streaming=True) flag during the 1.x line.
lf.explain(optimized=True)- Prints the physical plan. Read it bottom up and confirm the projection and selection appear at the scan node, which proves pushdown happened.
lf.profile()- Returns the result plus a per-node timing frame, the fastest way to learn which join is actually consuming the runtime.
df.lazy().filter(...).collect()- Wrapping an eager frame still buys cross-operation optimization. The tradeoff is that errors surface at collect rather than on the offending line.
pl.Config.set_streaming_chunk_size(50_000)- Smaller batches cut peak memory in the streaming engine at the cost of per-batch overhead. Tune it only after a real out-of-memory failure.
Interop and gotchas
df.to_pandas(use_pyarrow_extension_array=True)- Preserves nulls and avoids a copy where the memory layout allows. Without the flag an integer column containing nulls degrades to float64.
df.to_arrow()- Free, because polars already stores Arrow memory. This is the cheapest handoff to DuckDB, an Iceberg writer, or an Arrow Flight endpoint.
pl.from_pandas(pdf, include_index=False)- The pandas index is dropped unless requested, because polars has no index concept. Row position is the only ordering that exists.
df.null_count()- polars keeps null and NaN separate, so a float column can hold both. is_null does not match NaN and is_nan does not match null.
df.estimated_size("mb")- Reports the in-memory Arrow footprint. Compare it against the compressed Parquet size before assuming an eager read will fit.
From DataLane — tutorials at/blog, practice SQL live in theplayground.