Skip to content

Selecting Columns & Rows

How to pick specific data from a DataFrame. The single most-used skill in Pandas.

Quick reference

Goal Code
One column → Series df["col"] or df.col
Multiple columns df[["c1", "c2"]]
Row by label df.loc["row_label"]
Row by position df.iloc[0]
Specific cell by label df.loc["row_label", "col"]
Specific cell by position df.iloc[0, 1]
Boolean filter df[df.col > 5]

Set up

import pandas as pd

df = pd.DataFrame({
    "name":   ["Alice", "Bob", "Carol", "Dave", "Eve"],
    "age":    [25, 30, 35, 40, 45],
    "city":   ["Mumbai", "Delhi", "Mumbai", "Pune", "Delhi"],
    "salary": [50000, 60000, 75000, 90000, 100000],
}, index=["a", "b", "c", "d", "e"])

print(df)

We'll use this for the rest of the chapter.

Selecting columns

import pandas as pd

df = pd.DataFrame({
    "name":   ["Alice", "Bob", "Carol"],
    "age":    [25, 30, 35],
    "salary": [50000, 60000, 75000],
})

# Single column → Series
print(df["name"])
print(type(df["name"]).__name__)        # Series
print()

# Same — attribute access (only works for valid Python names)
print(df.name)
print()

# Multiple columns → DataFrame
print(df[["name", "salary"]])
print(type(df[["name", "salary"]]).__name__)   # DataFrame

Heads up: df["name"] works always. df.name works only if the column name is a valid Python identifier (no spaces, no leading digits, not a reserved word).

Selecting rows by label.loc[]

import pandas as pd

df = pd.DataFrame({
    "age": [25, 30, 35, 40, 45],
    "city": ["Mumbai", "Delhi", "Mumbai", "Pune", "Delhi"],
}, index=["a", "b", "c", "d", "e"])

# Single row by label
print(df.loc["b"])
print(type(df.loc["b"]).__name__)         # Series

# Multiple rows
print()
print(df.loc[["a", "c", "e"]])

# Range of labels — INCLUSIVE on both ends
print()
print(df.loc["b":"d"])

Selecting rows by position.iloc[]

import pandas as pd

df = pd.DataFrame({
    "age": [25, 30, 35, 40, 45],
    "city": ["Mumbai", "Delhi", "Mumbai", "Pune", "Delhi"],
}, index=["a", "b", "c", "d", "e"])

# Single row by position
print(df.iloc[0])

# First 3 rows — EXCLUSIVE on the end (like Python lists)
print()
print(df.iloc[0:3])

# Last 2 rows
print()
print(df.iloc[-2:])

# Pick specific positions
print()
print(df.iloc[[0, 2, 4]])

.loc vs .iloc — the key difference

.loc .iloc
Selects by label integer position
Slice endpoint inclusive exclusive (like Python)
Default index (0,1,2,…) uses those numbers as labels uses them as positions — looks the same
Custom index uses your labels still uses 0,1,2 positions

Rule of thumb: use .loc 90% of the time. .iloc is for "first 100 rows" / "every 10th row" type ops.

Cell selection — row + column at once

import pandas as pd

df = pd.DataFrame({
    "age": [25, 30, 35],
    "city": ["Mumbai", "Delhi", "Pune"],
}, index=["a", "b", "c"])

# loc: [row_label, col_label]
print(df.loc["b", "age"])              # 30

# iloc: [row_pos, col_pos]
print(df.iloc[1, 0])                   # 30

# Slice of rows × specific cols (loc)
print(df.loc["a":"b", "city"])

# Slice of rows × slice of cols
print(df.iloc[0:2, 0:2])

at and iat — single-cell access (fast)

For getting / setting one cell, these are faster than .loc / .iloc:

import pandas as pd

df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, index=["x", "y", "z"])

print(df.at["y", "a"])      # 2
print(df.iat[1, 0])         # 2

# Set
df.at["y", "a"] = 99
print(df)

Boolean filtering — df[condition]

The bread-and-butter row filter:

import pandas as pd

df = pd.DataFrame({
    "name":   ["Alice", "Bob", "Carol", "Dave"],
    "age":    [25, 30, 35, 40],
    "salary": [50000, 60000, 75000, 90000],
})

# Rows where age > 30
print(df[df["age"] > 30])
print()

# Rows where age > 30 AND salary > 70000
print(df[(df["age"] > 30) & (df["salary"] > 70000)])
print()

# Rows where city is one of several
df2 = pd.DataFrame({
    "name": ["Alice","Bob","Carol","Dave"],
    "city": ["Mumbai","Delhi","Pune","Mumbai"],
})
print(df2[df2["city"].isin(["Mumbai", "Pune"])])

Always wrap conditions in parens for & / |. Don't use and/or — those don't work element-wise.

.query() — readable filtering

For complex conditions:

import pandas as pd

df = pd.DataFrame({
    "name":   ["Alice", "Bob", "Carol", "Dave"],
    "age":    [25, 30, 35, 40],
    "salary": [50000, 60000, 75000, 90000],
})

# Use column names like variables
print(df.query("age > 30 and salary > 70000"))
print()

# Reference Python vars with @
min_age = 30
print(df.query("age > @min_age"))

Selecting by both row condition AND column subset

import pandas as pd

df = pd.DataFrame({
    "name":   ["Alice", "Bob", "Carol", "Dave"],
    "age":    [25, 30, 35, 40],
    "city":   ["Mumbai", "Delhi", "Pune", "Mumbai"],
    "salary": [50000, 60000, 75000, 90000],
})

# Rows where age>30, only the name and salary columns
print(df.loc[df["age"] > 30, ["name", "salary"]])

This is the most useful pattern — combining a filter with column selection.

Slice with step

import pandas as pd

df = pd.DataFrame({"x": range(10)})

# Every 2nd row
print(df.iloc[::2])

# Reverse
print(df.iloc[::-1])

Setting values via .loc

import pandas as pd

df = pd.DataFrame({
    "name":   ["Alice", "Bob", "Carol"],
    "age":    [25, 30, 35],
    "active": [True, True, True],
})

# Set a single cell
df.loc[0, "age"] = 26
print(df)

# Set a column based on a condition
df.loc[df["age"] > 30, "active"] = False
print(df)

# Set multiple columns at once
df.loc[df["age"] < 30, ["age", "active"]] = [20, False]
print(df)

Always use .loc for assignmentdf[df.x > 5]["col"] = 99 gives the dreaded SettingWithCopyWarning and may not actually work.

Combining selection — practical example

import pandas as pd
import numpy as np

rng = np.random.default_rng(0)
n = 12
df = pd.DataFrame({
    "id":     range(1, n+1),
    "name":   [f"User{i}" for i in range(1, n+1)],
    "city":   rng.choice(["Mumbai", "Delhi", "Pune"], size=n),
    "age":    rng.integers(20, 60, size=n),
    "salary": rng.integers(40_000, 150_000, size=n),
})

# Find high earners in Mumbai
print("High earners in Mumbai:")
print(df.loc[
    (df["city"] == "Mumbai") & (df["salary"] > 100_000),
    ["name", "age", "salary"]
])

Cheatsheet — pick the right tool

If you need to... Use
One column as Series df["col"]
Multiple columns df[["c1", "c2"]]
Row by label df.loc["label"]
Row by position df.iloc[0]
Cell by label df.loc["row", "col"] or df.at["row", "col"]
Cell by position df.iloc[0, 0] or df.iat[0, 0]
Filter rows df[df["col"] > 5] or df.query("col > 5")
Filter rows + pick cols df.loc[mask, ["c1", "c2"]]
Set a value df.loc[mask, "col"] = value
Membership filter df[df["c"].isin([...])]
Negation df[~df["c"].isin([...])]

Common pitfalls

  • Using df[df.x > 5]["col"] = 99 — gives SettingWithCopyWarning. Always assign via .loc.
  • and / or in conditions — element-wise needs & / |. And wrap each condition in parens.
  • Mixing labels and positionsdf.loc[0:3] with a custom index is by label and INCLUSIVE; df.iloc[0:3] is by position and EXCLUSIVE. Different results.
  • df.name failingname is a special attribute. Always use df["name"] for clarity.
  • Forgetting .copy() for new datasetssub = df[df.x > 5] is a view. Modifying sub can affect df. Use .copy() if you want independence.

Practice

What does this print?

Expected: 3

import pandas as pd
df = pd.DataFrame({"a": [10, 20, 30]}, index=["x", "y", "z"])
print(len(df.loc["x":"z"]))     # loc slice INCLUSIVE

Set the 'active' column to False for all rows where age > 30 (without warnings)

Expected: False

import pandas as pd
df = pd.DataFrame({"age": [25, 35, 40], "active": [True, True, True]})
df[df["age"] > 30]["active"] = False     # bug: chained assignment doesn't work
print(df.loc[1, "active"])

Quiz — Quick check

What you remember

Q1. When you slice with .loc, is the end label inclusive or exclusive?

  • Exclusive — like Python lists
  • Inclusive — both ends are included
  • Depends on the dtype
  • Configurable

Why: df.loc["a":"d"] includes rows with labels "a" through "d". df.iloc[0:4] excludes index 4 (like Python). This trips up everyone the first time.

Q2. When should you use .iloc instead of .loc?

  • Always — it's faster
  • When you want positional access (first N rows, every other row, etc.) independent of the labels
  • Only for boolean masks
  • Never — .loc is enough

Why: .loc is for label-based selection; .iloc is for position-based. Most filtering and assignment uses .loc; .iloc is best for index-aware operations like "first 100 rows".

Q3. Which raises SettingWithCopyWarning?

  • df.loc[df.x > 5, "y"] = 99
  • df[df.x > 5]["y"] = 99
  • df["y"] = 99
  • None

Why: Chained assignment (df[...][...] = ...) operates on a temporary copy, so the assignment doesn't propagate to df. Use the single-step .loc form to guarantee it works.

Common doubts

Is df.col ever a bad idea?

Yes — attribute access fails for column names that aren't valid Python identifiers (df["full name"], df["1st_quarter"]). It also collides with Pandas method names (df.shape, df.index — these are attributes of the DataFrame, not columns). Bracket-style df["col"] always works and is unambiguous.

What does SettingWithCopyWarning mean and how do I fix it?

Pandas isn't sure whether your assignment will modify the original DataFrame or a temporary copy. To make it unambiguous, always use .loc[mask, column] = value (single-step). Avoid df[df.x > 5]["col"] = value (two-step).

When should I use .at and .iat?

For getting or setting a single cell. .at/.iat are faster than .loc/.iloc for that case. For multi-cell operations, .loc/.iloc are required.

What's next

Filtering with Conditions