Polars 2.0 made the streaming engine the default and promoted SQL to a first-class feature. Polars 2.1, released on 25 October 2026, builds on both: group-bys and window functions can now spill to disk when data does not fit in memory, the SQL interface gains recursive queries, ASOF joins and temporary tables, and Polars can now create, overwrite and delete Apache Iceberg tables. The Polars team counts close to 90 performance improvements. We installed it and ran the examples below.
Group-bys that spill to disk
With the streaming engine, a group-by with millions of distinct keys used to need all those groups in memory. In 2.1 the group-by and the over window function can run out of core: when memory runs short, Polars writes partial results to disk and reads them back. You can set a memory budget with an environment variable and turn on metrics to see what happened.
import os
os.environ["POLARS_OOC_MEMORY_BUDGET_MB"] = "50" # set before importing Polars
os.environ["POLARS_OOC_LOG_METRICS"] = "1"
import polars as pl
n = 8_000_000
lf = pl.LazyFrame({
"k": pl.int_range(n, eager=True).shuffle(seed=0),
"v": pl.int_range(n, eager=True),
})
result = lf.group_by("k").agg(pl.col("v").sum()).collect()On our 2-core test machine, 8 million unique groups with a 50 MB budget finished in 2.3 seconds. The metrics showed about 35 MB written to disk and read back, with every spill succeeding. For analysts this means big aggregations on a laptop are far less likely to crash with an out-of-memory error.
Polars SQL catches up
If you prefer SQL to expressions, 2.1 closes many gaps. Recursive CTEs, ASOF joins, temporary tables and common date and text functions all work now.
import polars as pl
# Recursive CTE: count from 1 to 5
pl.SQLContext().execute("""
WITH RECURSIVE nums(n) AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM nums WHERE n < 5
)
SELECT n FROM nums
""").collect()
# ASOF join: the latest quote at or before each trade
trades = pl.DataFrame({"t": [1, 5, 10], "sym": ["A", "A", "A"], "qty": [100, 200, 300]})
quotes = pl.DataFrame({"t": [0, 4, 9], "sym": ["A", "A", "A"], "price": [10.0, 10.5, 11.0]})
ctx = pl.SQLContext(trades=trades, quotes=quotes)
ctx.execute("""
SELECT trades.t, qty, price
FROM trades
ASOF JOIN quotes MATCH_CONDITION (trades.t >= quotes.t)
ON trades.sym = quotes.sym
""").collect()
# prices 10.0, 10.5 and 11.0- WITH RECURSIVE
- Recursive CTEs for hierarchies, such as org charts or bills of materials, and number sequences.
- ASOF JOIN
- Match each row to the nearest earlier row, as in Snowflake and DuckDB, using MATCH_CONDITION.
- CREATE TEMP TABLE
- Save an intermediate result inside an SQLContext and query it later.
- generate_series and range
- Build sequences of numbers directly in SQL.
- DATE_TRUNC and CURRENT_DATE
- Truncate dates to a month or week and get today's date. DATE_TRUNC returns a datetime.
- REGEXP_REPLACE
- Replace text with a pattern. In our test it replaced the first match only, like PostgreSQL without the g flag.
- Subqueries in JOIN ... ON
- Correlated and uncorrelated subqueries now work inside join conditions.
Apache Iceberg: from reading to managing tables
Polars could already read and append to Iceberg tables. 2.1 adds most of what you need to manage them:
Writing
sink_iceberg- create_if_not_exists and create_namespace_if_not_exists
- overwrite_filter and dynamic partition overwrite
- Tables with bucket-partitioned fields
Rebuild only the partitions you changed.
Catalogs
SQL, unstable- register_catalog attaches an Iceberg catalog to an SQLContext
- CREATE TABLE with column definitions and DROP TABLE [PURGE]
- pl.delete_iceberg to delete rows
- Marked unstable, so the API may still change
Manage Iceberg tables without Spark for small and medium workloads.
New to Iceberg? Read Apache Iceberg v3, explained.
Smaller additions
- Encrypted Parquet files can be read and written (unstable), and GeoParquet read and write is experimental.
join_wheresupports full joins, so inequality joins can keep unmatched rows on both sides.Expr.truncdivdivides and rounds towards zero: -7 divided by 2 gives -3, where//gives -4.- Faster creation of wide struct columns and much lower memory use in
from_rowsandfrom_dicts. - Filters using
str.containswith a literal, and SQLLIKE, can now skip Parquet data early.
Breaking changes
Only two. First, Python 3.10 is no longer supported. Second, adding a number to a string in eager mode now raises an error instead of joining them as text:
pl.Series([1, 2]) + "4"
# Polars 2.0: ["14", "24"]
# Polars 2.1: InvalidOperationError: arithmetic on dtypes i64 and str is not allowedIf you relied on that, cast explicitly with .cast(pl.String) and use pl.concat_str.
How to upgrade
- 1
Check your Python version
Polars 2.1 needs Python 3.11 or newer.
- 2
Upgrade
Run pip install -U polars, or uv add polars.
- 3
Search for string arithmetic
Look for code that adds numbers and strings, and make the cast explicit.
- 4
Try a memory budget
For large group-bys, set POLARS_OOC_MEMORY_BUDGET_MB and compare speed and stability.
- 5
Avoid unstable APIs in production
Encryption, register_catalog and delete_iceberg may change in later releases.
Check your understanding
1/3What does out-of-core group-by let Polars do?
The group-by and over window function can now run with a memory budget smaller than the data.
2/3Which SQL feature was added in Polars 2.1?
Recursive CTEs, ASOF joins and temporary tables are all new in 2.1.
3/3What happens to pl.Series([1, 2]) + "4" in Polars 2.1?
Mixing numbers and strings in arithmetic now raises; cast explicitly instead.
You got 0 out of 3.
More on Python data tools: what's new in Polars 2.0, what's new in pandas 3.1, pandas vs Polars in 2026 and the Polars cheat sheet.
Sources: Polars blog, "Announcing Polars 2.1" (27 October 2026), the Polars 2.1.0 release on PyPI (25 October 2026), and our own tests with Polars 2.0.0 and 2.1.0 on Python 3.13. Timings are from one 2-core machine and are indicative only.
