Pre-Release of Polars 2.0
🚀 Pre-Release of Polars 2.0
By Ritchie Vink | Published Wednesday, Sept 2, 2026
We are thrilled to announce the launch of the first Release Candidate (RC) for Polars 2.0.
Why a Major Version Bump?
The transition to 2.0 isn't just a number change. We are leveraging this major release to:
Discard legacy design constraintsEliminate architectural bottlenecks that previously hindered our progress.- Optimize Defaults: Shift to settings that provide the most benefit to the widest range of users.
The most significant shift is that all LazyFrame queries will now execute on the streaming engine by default.
The Bottom Line: For the average user, this means a massive leap in performance and a significant reduction in memory overhead. We anticipate the streaming engine to be faster in aggregate.
To assist with this transition, we have provided a comprehensive [migration guide].
🌊 The Streaming Engine as Default
In Polars 2.0, calling .collect() on a LazyFrame automatically triggers the streaming engine. While this provides immense speed, it introduces a breaking change regarding data sequencing.
The Trade-off: Row Order
The streaming engine does not guarantee the preservation of row-order for specific operations, including:
joingroup_byunpivot
How to handle row-order requirements:
If your pipeline relies on observable row-order, you have three primary options:
- Query-level Opt-in: Use
maintain_order=Truewithin the specific operation. - Global Fallback: Set the engine affinity for the entire process.
- Per-query Fallback: Specify the engine during collection.
Code Implementation Example:
import polars as pl
lf = pl.LazyFrame({"k": [2, 1, 0], "v": ["a", "b", "c"]})
other = pl.LazyFrame({"k": [0, 1, 2], "r": ["x", "y", "z"]})
# 2.0 Default: engine="auto" uses streaming.
# Note: Row order may vary from the original lf.
result = lf.collect()
# Option A: Maintain order for a specific join
ordered_result = (
lf.join(other, on="k", how="left", maintain_order="left")
.collect()
)
# Option B: Global switch back to in-memory
pl.set_engine_affinity("in-memory")
# Option C: Per-query switch
result_mem = lf.collect(engine="in-memory")
🛡️ A Stricter Polars
Polars is evolving to be more rigorous. Our philosophy is to fail fast. It is far better for a pipeline to crash immediately than to run for 20 minutes only to produce incorrect data due to a silent mismatch.
"Implicit behavior on data-mismatches should be opt-in, not a default, since those mismatches can hide bugs."
AI-Driven Development & Schema Validation
This strictness is particularly beneficial for AI agents. By utilizing collect_schema(), agents can:
- Resolve types early.
- Catch schema-level errors without materializing data.
- Iterate rapidly through query structures.
Examples of Increased Strictness
1. Lossless Type-Coercion in is_in
Previously, is_in would cast different data types to a common supertype, even if that cast was lossy.
The Danger Zone: Imagine checking a user ID against a list of flagged IDs loaded from JSON (where large integers became floats).
- User ID:
9007199254740993 - Float64 limit:
In 1.x, the ID would be coerced to Float64, silently rounding down and creating a false positive. In 2.0, this triggers an InvalidOperationError.
2. Strict Horizontal Concatenation
Horizontal concatenation no longer silently fills missing rows with null.
| Feature | Polars 1.x (Implicit) | Polars 2.0 (Strict) |
|---|---|---|
| Mismatched Lengths | Fills with null | Raises ShapeError |
| Behavior | Silent failure/padding | Explicit crash |
| Solution | N/A | Use how="horizontal_extend" |
# This will now raise a ShapeError in 2.0
transactions = pl.DataFrame({"day": [1, 2, 3, 4, 5], "count": [120, 98, 143, 87, 156]})
fraud_flags = pl.DataFrame({"flagged": [2, 0, 5, 1]}) # Only 4 rows
# pl.concat([transactions, fraud_flags], how="horizontal") # ❌ ERROR
3. Removal of Ambiguous Casts
We have removed several .cast() options in favor of dedicated constructors to ensure there is only one "obvious" way to parse data.
Use.cast(pl.Enum).cat.to(dtype)(for int categorical) or.cat.physical()(for categorical int).Use dedicated parsing expressions to maintain control over date formats..cast(pl.Date)
🛠️ Improved Error Reporting
To ensure a smooth transition for humans and AI agents alike, we've introduced more descriptive exceptions for deprecated features:
-
polars.exceptions.AttributeRemovedError: Triggered when a removed attribute or method is accessed. -
polars.exceptions.ArgumentRemovedError: Triggered when a removed parameter is passed to a function.
Example: If you attempt to use a removed parameter in the melt function, you will now receive a specific AttributeRemovedError rather than a generic failure.
(Placeholder for Polars Branding)