Last modified: May 10, 2026 By Alexander Williams
Polars Expression System Core Concept
Polars is a lightning-fast DataFrame library for Python. Its speed comes from a unique design. The heart of that design is the expression system. This is not just another API. It is a fundamental shift in how you think about data manipulation.
In this article, we will explore what expressions are. We will see why they matter. You will learn how to use them to write clean, fast, and powerful code. This guide is perfect for beginners and anyone switching from Pandas.
What is a Polars Expression?
An expression in Polars is a description of a computation. It does not perform the computation right away. Instead, it defines a transformation. This is called lazy evaluation.
Think of an expression like a recipe. The recipe tells you what to do with ingredients. You only cook when you are ready. Polars works the same way. You build a chain of expressions. Then you execute them all at once.
Every expression operates on a Series or a column. This is a key difference from Pandas. In Pandas, you often write operations on entire DataFrames. In Polars, you write expressions on columns. This makes the logic very clear.
Why Expressions Make Polars Fast
Polars is written in Rust. This gives it a huge speed advantage. But the expression system adds another layer of optimization. When you chain expressions, Polars can see the whole query. It can then optimize the entire plan.
For example, Polars can push down predicates. This means filters are applied as early as possible. It also can eliminate unnecessary columns. This reduces memory usage and I/O. These optimizations are automatic. You get them for free.
In Pandas, you often create intermediate DataFrames. Each step takes time and memory. Polars avoids this by using a query planner. The planner builds a logical plan. Then it creates an optimized physical plan. This is why Polars can be 10 to 100 times faster than Pandas for many tasks.
If you are curious about the speed difference, read our detailed comparison: Polars vs Pandas: Why Switch?
Building Your First Expression
Let's start with a simple example. We will create a DataFrame and use an expression. The most common expression is pl.col(). This selects a column.
import polars as pl
# Create a simple DataFrame
df = pl.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"age": [25, 30, 35],
"salary": [50000, 60000, 70000]
})
# Use an expression to select a column
result = df.select(pl.col("name"))
print(result)
shape: (3, 1)
┌─────────┐
│ name │
│ --- │
│ str │
╞═════════╡
│ Alice │
│ Bob │
│ Charlie │
└─────────┘
The select method takes one or more expressions. Here, pl.col("name") is an expression. It tells Polars to select the "name" column. The output is a new DataFrame with only that column.
Chaining Expressions
The real power comes from chaining. You can combine multiple expressions in one select. You can also use expressions in other methods like filter and with_columns.
Let's create an expression that computes a new column. We will add a bonus to the salary.
# Chain expressions to create a new column
result = df.with_columns(
(pl.col("salary") * 1.10).alias("salary_with_bonus")
)
print(result)
shape: (4, 4)
┌─────────┬─────┬────────┬────────────────────┐
│ name ┆ age ┆ salary ┆ salary_with_bonus │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 ┆ f64 │
╞═════════╪═════╪════════╪════════════════════╡
│ Alice ┆ 25 ┆ 50000 ┆ 55000.0 │
│ Bob ┆ 30 ┆ 60000 ┆ 66000.0 │
│ Charlie ┆ 35 ┆ 70000 ┆ 77000.0 │
└─────────┴─────┴────────┴────────────────────┘
Notice the alias method. This renames the output column. Without it, the new column would be named after the expression. That is often ugly. Using alias is a best practice.
Filtering with Expressions
Filtering rows is another common task. You use the filter method with an expression. The expression must return a boolean Series.
# Filter rows where age is greater than 28
result = df.filter(pl.col("age") > 28)
print(result)
shape: (2, 3)
┌─────────┬─────┬────────┐
│ name ┆ age ┆ salary │
│ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 │
╞═════════╪═════╪════════╡
│ Bob ┆ 30 ┆ 60000 │
│ Charlie ┆ 35 ┆ 70000 │
└─────────┴─────┴────────┘
You can combine multiple conditions. Use the & operator for AND. Use the | operator for OR. Remember to wrap each condition in parentheses.
# Filter for age > 28 AND salary > 55000
result = df.filter(
(pl.col("age") > 28) & (pl.col("salary") > 55000)
)
print(result)
shape: (2, 3)
┌─────────┬─────┬────────┐
│ name ┆ age ┆ salary │
│ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 │
╞═════════╪═════╪════════╡
│ Bob ┆ 30 ┆ 60000 │
│ Charlie ┆ 35 ┆ 70000 │
└─────────┴─────┴────────┘
For more details on filtering and selecting, see our guide: Select Columns & Filter Rows in Polars.
Aggregations with Expressions
Aggregations are a core part of data analysis. Polars makes them very expressive. You can compute multiple aggregations in one call. Use the group_by method followed by agg.
# Group by name and compute aggregations
result = df.group_by("name").agg([
pl.col("salary").mean().alias("avg_salary"),
pl.col("age").max().alias("max_age")
])
print(result)
shape: (3, 3)
┌─────────┬────────────┬─────────┐
│ name ┆ avg_salary ┆ max_age │
│ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ i64 │
╞═════════╪════════════╪═════════╡
│ Alice ┆ 50000.0 ┆ 25 │
│ Bob ┆ 60000.0 ┆ 30 │
│ Charlie ┆ 70000.0 ┆ 35 │
└─────────┴────────────┴─────────┘
You can pass a list of expressions to agg. Each expression is a computation on a column. This is much cleaner than writing separate aggregation calls.
Lazy and Eager Execution
Polars has two modes: lazy and eager. So far, we have used eager mode. Every command runs immediately. But lazy mode is where the magic happens.
To use lazy mode, start with pl.LazyFrame() or use the .lazy() method on a DataFrame. Then build your expression chain. Finally, call .collect() to execute.
# Lazy execution example
lazy_df = df.lazy()
query = (lazy_df
.filter(pl.col("age") > 25)
.with_columns((pl.col("salary") * 1.05).alias("new_salary"))
.group_by("name")
.agg(pl.col("new_salary").mean())
)
# Execute the query
result = query.collect()
print(result)
shape: (2, 2)
┌─────────┬────────────┐
│ name ┆ new_salary │
│ --- ┆ --- │
│ str ┆ f64 │
╞═════════╪════════════╡
│ Bob ┆ 63000.0 │
│ Charlie ┆ 73500.0 │
└─────────┴────────────┘
Lazy mode allows Polars to optimize the entire query. It can reorder operations. It can combine filters. It can even avoid reading data you don't need. This is why lazy mode is recommended for complex workflows.
Expressions Are Reusable
One of the best features of expressions is reusability. You can define an expression once and use it many times. This makes your code cleaner and less error-prone.
# Define a reusable expression
high_salary = pl.col("salary") > 55000
age_bonus = (pl.col("age") * 100).alias("age_bonus")
# Use the expressions in different contexts
filtered = df.filter(high_salary)
with_bonus = df.with_columns(age_bonus)
print(filtered)
print(with_bonus)
shape: (2, 3)
┌─────────┬─────┬────────┐
│ name ┆ age ┆ salary │
│ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 │
╞═════════╪═════╪════════╡
│ Bob ┆ 30 ┆ 60000 │
│ Charlie ┆ 35 ┆ 70000 │
└─────────┴─────┴────────┘
shape: (3, 4)
┌─────────┬─────┬────────┬───────────┐
│ name ┆ age ┆ salary ┆ age_bonus │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 ┆ i64 │
╞═════════╪═════╪════════╪═══════════╡
│ Alice ┆ 25 ┆ 50000 ┆ 2500 │
│ Bob ┆ 30 ┆ 60000 ┆ 3000 │
│ Charlie ┆ 35 ┆ 70000 ┆ 3500 │
└─────────┴─────┴────────┴───────────┘
This is a huge advantage over Pandas. In Pandas, you often repeat the same logic. In Polars, you define it once and reuse it.
Combining Expressions with Contexts
Expressions work in different contexts. The most common contexts are select, with_columns, filter, and group_by. Each context has a specific purpose.
- select: Choose which columns to keep.
- with_columns: Add or modify columns.
- filter: Keep rows that match a condition.
- group_by: Split data into groups for aggregation.
You can also use expressions in the sort method. For example, df.sort(pl.col("age")) sorts by age. This is consistent with the expression system. For more on sorting, see Polars DataFrame Sorting & Slicing Guide.
Working with Multiple Columns
Often you need to apply the same operation to many columns. Polars makes this easy. You can use the pl.all() expression to select all columns. Or use pl.col("^pattern$") with a regex.
# Multiply all numeric columns by 2
result = df.select(
pl.col("name"),
pl.col("^age|salary$").mul(2)
)
print(result)
shape: (3, 3)
┌─────────┬─────┬────────┐
│ name ┆ age ┆ salary │
│ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 │
╞═════════╪═════╪════════╡
│ Alice ┆ 50 ┆ 100000 │
│ Bob ┆ 60 ┆ 120000 │
│ Charlie ┆ 70 ┆ 140000 │
└─────────┴─────┴────────┘
This is very powerful. You can also use pl.exclude() to select all columns except some. For column manipulation, check our guide: Polars Columns: Add, Rename & Drop.
Error Handling with Expressions
Expressions are type-safe. If you try to do an invalid operation, Polars will give a clear error. For example, you cannot add a string to a number.
# This will raise an error
try:
result = df.with_columns(
pl.col("name") + pl.col("age")
)
except Exception as e:
print(f"Error: {e}")
Error: PolarsError: cannot add 'str' and 'i64'
This is much better than Pandas, which often silently returns NaN or garbage. Polars catches errors early. This makes debugging much easier.
Expressions and Window Functions
Polars supports window functions through expressions. These are useful for running totals, ranks, and more. You use the .over() method to specify the window.
# Add a rank within each group
result = df.with_columns(
pl.col("salary").rank("dense").over("name").alias("salary_rank")
)
print(result)
shape: (3, 4)
┌─────────┬─────┬────────┬──────────────┐
│ name ┆ age ┆ salary ┆ salary_rank │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 ┆ u32 │
╞═════════╪═════╪════════╪══════════════╡
│ Alice ┆ 25 ┆ 50000 ┆ 1 │
│ Bob ┆ 30 ┆ 60000 ┆ 1 │
│ Charlie ┆ 35 ┆ 70000 ┆ 1 │
└─────────┴─────┴────────┴──────────────┘
Since each name appears only once, the rank is 1 for all. In a real dataset with duplicates, this would be very useful.
Conclusion
The Polars expression system is a game-changer. It makes data manipulation fast, clean, and reusable. By thinking in terms of expressions, you write better code.
We covered the core concepts: what expressions are, how to chain them, and how to use them in different contexts. You learned about lazy vs eager execution. You also saw how to reuse expressions and handle errors.
Now it is your turn. Start using expressions in your Polars code. You will see the difference in speed and clarity. For a deeper dive, explore our other guides. Learn how to Explore Data with Polars Shape, Head and Read & Write Files with Polars.
Happy coding with Polars!