Polars Primer 02: Isolating Rows and Columns

Polars
Python
Rust
Data Analytics
A look at Polars using Python and Rust
Author

Dennis Chua

Published

August 4, 2026

Projecting Rows and Columns of Data

All code snippets use Rent_Contracts.csv as input data, sourced from lemoninabag/Rentals · Datasets at Hugging Face.

When working with Polars, we interact with data organized as DataFrames or Series. A Polars Series is a single-column structure that exists as a standalone array. A Polars DataFrame is a two dimensional structure aggregating several Series arrays into a table of rows and columns.

Technically speaking, a Polars Series has these basic characteristics:

  1. It is a single column of data with a uniform data type.

  2. Every entry in a column is associated with a row index.

In contrast, DataFrame, which we’ve been dealing with so far, holds data of potentially mixed data types. A DataFrame is also a two-dimensional structure, indexed by row index and column name.

Selecting Rows

When working in eager execution mode, Polars uses the array-like convention of indexing into an in-memory DataFrame starting with zero. We retrieve the third row by means of the bracket notation:

polars_data[2]

Similarly, we access the n-th row using the expression polars_data[n]. In Rust, we aren’t afforded the bracket notation. Instead we use the slice() operator. The left index indicates the starting row; the right index, however, indicates the offset, the number of rows to retrieve, including the starting row.

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;
        
    let third_row_df = rental_lazy.slice(2, 1).collect()?;

    // Print the resulting DataFrame
    println!("{:?}", third_row_df);

    Ok(())
}
shape: (1, 30)
┌────────────┬────────────┬────────────┬────────────┬───┬───────────┬───────────┬───────────┬──────────┐
 contract_i ┆ contract_r ┆ contract_r ┆ contract_s ┆ … ┆ tenant_ty ┆ tenant_ty ┆ project_n ┆ rooms_en │
 d          ┆ eg_type_id ┆ eg_type_en ┆ tart_date  ┆   ┆ pe_id     ┆ pe_en     ┆ ame       ┆ ---
 ------------        ┆   ┆ ---------       ┆ str      │
 str        ┆ i64        ┆ str        ┆ str        ┆   ┆ f64       ┆ str       ┆ str       ┆          │
╞════════════╪════════════╪════════════╪════════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡
 CNT2126820 ┆ 2          ┆ Renew      ┆ 2025-12-31 ┆ … ┆ 1.0       ┆ Person    ┆ Meydan    ┆ 2 B/R    │
 013        ┆            ┆            ┆            ┆   ┆           ┆           ┆           ┆          │
└────────────┴────────────┴────────────┴────────────┴───┴───────────┴───────────┴───────────┴──────────┘

As with idiomatic Python, we access the last row of a DataFrame by means of the index -1.

polars_data[-1]

With Rust in lazy execution mode, we rely on the LazyFrame tail() operation to fetch the last row.

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    // Select the last row and execute the query
    let last_row_df = rental_lazy.tail(1).collect()?;

    // Print the resulting DataFrame
    println!("{:?}", last_row_df);

    Ok(())
}

How can we select a bunch of rows? Python lets us apply the range operation.

polars_data[1:8]

Once again, for Rust we use the slice() operator to isolate the seven rows from the data stream, including the second one.

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    // Python equivalent: polars_data[1:3]
    // Start at row index 1 (the second row) and take 7 rows total.
    let sliced_df = rental_lazy.slice(1, 7).collect()?;

    // Print the resulting DataFrame
    println!("{:?}", sliced_df);

    Ok(())
}

So in the above example, the bounds slice(1, 7), yields seven rows of data: the second until and including the eight, with every row in between.

shape: (7, 30)
┌────────────┬────────────┬────────────┬────────────┬───┬───────────┬───────────┬───────────┬──────────┐
 contract_i ┆ contract_r ┆ contract_r ┆ contract_s ┆ … ┆ tenant_ty ┆ tenant_ty ┆ project_n ┆ rooms_en │
 d          ┆ eg_type_id ┆ eg_type_en ┆ tart_date  ┆   ┆ pe_id     ┆ pe_en     ┆ ame       ┆ ---
 ------------        ┆   ┆ ---------       ┆ str      │
 str        ┆ i64        ┆ str        ┆ str        ┆   ┆ f64       ┆ str       ┆ str       ┆          │
╞════════════╪════════════╪════════════╪════════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡
 CNT1786418 ┆ 2          ┆ Renew      ┆ 2025-12-31 ┆ … ┆ 1.0       ┆ Person    ┆ Discovery ┆ 1 B/R    │
 853        ┆            ┆            ┆            ┆   ┆           ┆           ┆ Gardens   ┆          │
 CNT2126820 ┆ 2          ┆ Renew      ┆ 2025-12-31 ┆ … ┆ 1.0       ┆ Person    ┆ Meydan    ┆ 2 B/R    │
 013        ┆            ┆            ┆            ┆   ┆           ┆           ┆           ┆          │
 CNT2128949 ┆ 2          ┆ Renew      ┆ 2025-12-28 ┆ … ┆ 1.0       ┆ Person    ┆ null      ┆ 2 B/R    │
 309        ┆            ┆            ┆            ┆   ┆           ┆           ┆           ┆          │
 CNT2126929 ┆ 2          ┆ Renew      ┆ 2025-12-26 ┆ … ┆ 1.0       ┆ Person    ┆ DAMAC     ┆ 1 B/R    │
 158        ┆            ┆            ┆            ┆   ┆           ┆           ┆ HEIGHTS   ┆          │
 CNT2126854 ┆ 2          ┆ Renew      ┆ 2025-12-25 ┆ … ┆ 1.0       ┆ Person    ┆ null      ┆ Studio   │
 343        ┆            ┆            ┆            ┆   ┆           ┆           ┆           ┆          │
 CNT2126796 ┆ 1          ┆ New        ┆ 2025-12-25 ┆ … ┆ 1.0       ┆ Person    ┆ Internati ┆ 2 B/R    │
 385        ┆            ┆            ┆            ┆   ┆           ┆           ┆ onal City ┆          │
            ┆            ┆            ┆            ┆   ┆           ┆           ┆ Phase 1   ┆          │
 CNT2123582 ┆ 2          ┆ Renew      ┆ 2025-12-25 ┆ … ┆ 1.0       ┆ Person    ┆ MIRDIF    ┆ 2 B/R    │
 618        ┆            ┆            ┆            ┆   ┆           ┆           ┆ HILLS-    ┆          │
            ┆            ┆            ┆            ┆   ┆           ┆           ┆ JANAYEN   ┆          │
            ┆            ┆            ┆            ┆   ┆           ┆           ┆ AVENUE    ┆          │
└────────────┴────────────┴────────────┴────────────┴───┴───────────┴───────────┴───────────┴──────────┘

Selecting Columns

So far we’ve used indexing to extract rows from the data. Polars also allows use the same bracket notation to select columns. Python overloads the bracket operator to accept column names of string type.

polars_data["contract_id"]

As we’ve seen before, Rust doesn’t come with the convenience of the bracket operator. Instead we use the select() operation combined with col(). The col() implements Rust’s strict data type rules. It converts the column name, expressed as a static string ("contract_id"), into a Polars Expression (Expr) object, the building block of every Polars query engine.

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let contract_id_df = rental_lazy
        .select([col("contract_id")])
        .collect()?;
    println!("{:?}", contract_id_df);

    Ok(())
}
shape: (1_215_770, 1)
┌───────────────┐
 contract_id   │
 ---
 str           │
╞═══════════════╡
 CRT2128114436 │
 CNT1786418853 │
 CNT2126820013 │
 CNT2128949309 │
 CNT2126929158 │
 …             │
 CNT209080015  │
 CNT345228436  │
 CNT208147537  │
 CRT1182225816 │
 CNT152225829  │
└───────────────┘

Polars also allows us to select multiple columns. In Python we pass a list of column names.

polars_data["contract_id", "contract_start_date"]

For our Rust code, we pass a list of col() operators to our select().

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let subset_df = rental_lazy
        .select([
            col("contract_id"), 
            col("contract_start_date")
        ])
        .collect()?;

    println!("{:?}", subset_df);

    Ok(())
}
shape: (1_215_770, 2)
┌───────────────┬─────────────────────┐
 contract_id   ┆ contract_start_date │
 ------
 str           ┆ str                 │
╞═══════════════╪═════════════════════╡
 CRT2128114436 ┆ 2025-12-31          │
 CNT1786418853 ┆ 2025-12-31          │
 CNT2126820013 ┆ 2025-12-31          │
 CNT2128949309 ┆ 2025-12-28          │
 CNT2126929158 ┆ 2025-12-26          │
 …             ┆ …                   │
 CNT209080015  ┆ 2014-02-01          │
 CNT345228436  ┆ 2014-02-01          │
 CNT208147537  ┆ 2014-02-01          │
 CRT1182225816 ┆ 2013-06-20          │
 CNT152225829  ┆ 2013-02-25          │
└───────────────┴─────────────────────┘

DataFrame and Series Revisited

One way of looking at a two dimensional DataFrame is that it is made up of a collection of Series columns. Polars draws on this concept when it leverages Python’s list idiom, as seen in the code snippet below.

polars_data[1:3, ["contract_id","contract_start_date"]]

In that example, the Python bracket notation combines everything we’ve learned. Here we select two rows and two columns from our data. To accomplish this in Rust, we need to be more explicit, resort to chaining select() (to isolate columns by name) and slice() (to isolate rows by index) operations to assemble the DataFrame.

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let rental_lazy = LazyCsvReader::new("Rent_Contracts.csv".into())
        .with_has_header(true)
        .finish()?;

    let result_df = rental_lazy
        .select([
            col("contract_id"), 
            col("contract_start_date")
        ])
        .slice(1, 2) // Offset: 1 (start at row index 1), Length: 2 (take two rows)
        .collect()?;
    println!("{:?}", result_df);

    Ok(())
}
shape: (2, 2)
┌───────────────┬─────────────────────┐
 contract_id   ┆ contract_start_date │
 ------
 str           ┆ str                 │
╞═══════════════╪═════════════════════╡
 CNT1786418853 ┆ 2025-12-31          │
 CNT2126820013 ┆ 2025-12-31          │
└───────────────┴─────────────────────┘

It bears noting once more the difference between the eager execution versus the lazy execution. Although both Python and Rust examples produce a two-by-two DataFrame, the Rust lazy execution mode reads only these four cells from the input file. On the other, eager execution in the preceding Python snippet reads the entire data from the file before applying the filtering operations.

The Polars Query Engine

Unlike other data manipulation tools that update data structures in place, Polars lazy execution mode uses a query plan to lay out and optimize its execution. Before Polars reads data from the source (file on a drive, standard input, network socket, etc.), it refers to elements of the data stream using symbolic placeholders. In the case of ‘col(“contract_id”)’, Polars represents the actual data as an Expression (Expr) object. An Expr is a light weight operand, a shorthand Polars uses to chain complex operations, such as str().len_chars(), without harnessing the data from its source into memory. With optimization, Polars is able to filter the relevant data columns it needs, avoiding the overhead of manipulating the entire data stream, the default in eager execution mode.

The LazyFrame is an efficient way of processing large amounts of data. Polars uses the query plan to map out relationships between functions and operands so it can evenly distribute tasks across concurrent threads (Rayon). This pre-planning phase is how Polars in lazy execution mode achieves parallelism and high data throughput.