Select Data with loc and iloc

Learn to select columns and rows with loc and iloc in pandas. Master label-based and position-based indexing with hands-on examples, troubleshooting tips, and next steps in the Data Analysis with Python track.

Focus: select columns and rows with loc and iloc

Sponsored

You've loaded a DataFrame, glimpse at your data, and now you're staring at hundreds of columns and thousands of rows. Every time you need a specific slice — say, the price column for the last 50 customers — you find yourself writing clunky chain after chain of code, or worse, accidentally grabbing the wrong data because you confused a label with a position. That inefficiency is costing you time and risking silent errors. Selecting columns and rows with loc and iloc is the precise, fast, and readable way to extract exactly the data you need — and in this lesson, you'll master both so selection becomes second nature.

The problem this lesson solves

Without deliberate, consistent selection methods, data analysis code becomes a mess of guesswork. You might use df['column'] to get a column, but what about multiple columns? What about rows that match a condition? What about rows five through ten? Beginners often mix up label and position, leading to off-by-one errors or KeyErrors that are frustrating to debug.

These errors aren't just annoying — they undermine trust in your analysis. A single wrong row can skew an average, corrupt a merge, or mislead a business decision. The solution is to stop improvising and start using the two purpose-built pandas indexers: loc and iloc. Once you internalize the difference, you'll write selection code that is explicit, robust, and self-documenting.

By the end of this lesson, you'll not only be able to explain the core idea, but also apply it in a hands-on exercise — and you'll be ready to move on to the next step in the Data Analysis with Python track.

Core concept / mental model

Think of your DataFrame as a spreadsheet with labeled rows (the index) and labeled columns. There are two natural ways to refer to a cell or a block: by name or by number.

  • loc = location by label. You ask pandas: 'Give me the row whose index is 'A', and the column named 'price'.' The index label and column name can be any hashable object (string, integer, date, etc.). Crucially, if your index contains integers 0–9, loc[0] means the row labeled 0, not necessarily the first row.
  • iloc = integer location. You ask pandas: 'Give me the 0th row and the 2nd column.' Here, 0 means first, 1 means second, and so on — identical to Python list indexing, including negative values to count from the end.

A helpful analogy: loc is like using the postal address of a house, iloc is like using its coordinates on a map. Both can get you to the same place if the address and coordinates happen to align, but they are distinct systems.

The loc and iloc indexers return a new object (a view or copy depending on context), and they accept a wide range of inputs: single labels, lists of labels, slices (inclusive for loc, exclusive for iloc), boolean masks, and callables.

How it works step by step

The best way to master loc and iloc is to understand their grammar. Both follow the same syntax:

# General form
selection = df.loc[row_selector, column_selector]
selection = df.iloc[row_selector, column_selector]

Both selectors can be any of the following:

  • A single label (or integer for iloc): returns a scalar (if both are single) or a Series.
  • A list of labels (or integers): returns a DataFrame.
  • A slice: for loc, it's inclusive on both ends; for iloc, it's exclusive of the stop, just like Python lists.
  • A boolean Series or array (e.g., from a condition): returns rows where True.
  • A callable that takes the DataFrame and returns one of the above.

Let's unpack the steps with a concrete sequence:

  1. Identify your rows. Ask: do I know the row names? If yes, use loc. If I only know positions (e.g., the first 100 rows), use iloc.
  2. Identify your columns. Same logic: column names? Use loc. Column positions? Use iloc.
  3. Combine the two with a comma inside the square brackets. If you omit the column selector (e.g., df.loc[['a','b']]), pandas returns all columns for those rows.

You can also mix row selection with column selection in a single call, which is far more readable than chaining df[df['x']>0]['y'] (and also avoids potential SettingWithCopyWarning later).

Hands-on walkthrough

Let's build a small DataFrame and try every core selection pattern. This is the same kind of data you'll encounter in the next lessons in the track.

import pandas as pd

df = pd.DataFrame(
    {
        "product": ["Laptop", "Mouse", "Monitor", "Keyboard", "SSD"],
        "price": [1200, 25, 300, 80, 150],
        "stock": [10, 500, 200, 300, 150],
    },
    index=["A", "B", "C", "D", "E"],  # custom labels, just for demo
)

print(df)

Output:

    product  price  stock
A    Laptop   1200     10
B     Mouse     25    500
C   Monitor    300    200
D  Keyboard     80    300
E       SSD    150    150

Selecting columns with loc and iloc

# Single column by name (returns a Series)
print(df.loc[:, "price"])

# Multiple columns by name (returns a DataFrame)
print(df.loc[:, ["product", "price"]])

# Single column by position (the 2nd column, index 1)
print(df.iloc[:, 1])

# Multiple columns by position
print(df.iloc[:, [0, 2]])

Note that the row selector : (a slice from start to end) means 'all rows.' For iloc, you could also write df.iloc[range(len(df)), 1], but : is more idiomatic.

Selecting rows with loc and iloc

# Row by label (returns a Series)
print(df.loc["C"])

# Rows by list of labels (returns a DataFrame)
print(df.loc[["B", "D"]])

# Row by position (the 3rd row)
print(df.iloc[2])

# Rows by slice: label slice is INCLUSIVE -> B, C, D
print(df.loc["B":"D"])

# Position slice is EXCLUSIVE -> rows 1 and 2 (B and C)
print(df.iloc[1:3])

Output for the last two:

    product  price  stock
B     Mouse     25    500
C   Monitor    300    200
D  Keyboard     80    300

    product  price  stock
B     Mouse     25    500
C   Monitor    300    200

Combine rows and columns

This is where loc/iloc shine — one call, no chaining:

# Label-based: rows B and D, columns 'product' and 'price'
print(df.loc[["B", "D"], ["product", "price"]])

# Position-based: rows 1 and 3, columns 0 and 2
print(df.iloc[[1, 3], [0, 2]])

# Label slice with column list
print(df.loc["B":"C", "price"])

Use conditions with loc

Boolean masks are the bread and butter of real analyses:

# All rows where price > 100, select 'product' and 'price'
print(df.loc[df["price"] > 100, ["product", "price"]])

Expected output:

    product  price
A    Laptop   1200
C   Monitor    300
E       SSD    150

Pro tip: df[df['price'] > 100]['product'] works, but df.loc[df['price'] > 100, 'product'] is more explicit and keeps the selection in one place — and it avoids the SettingWithCopyWarning trap when you later assign values.

Compare options / when to choose what

Not every selection needs loc or iloc. Here's a quick comparison of your common tools.

Method Syntax When to use Inclusive slice? Fast?
df['col'] Square bracket with column name Get a single column (Series) N/A Yes
df[['col1','col2']] List of column names Get a DataFrame of multiple columns N/A Yes
df[df['col'] > 0] Boolean mask (rows only) Filter rows when you don't need column selection N/A Yes
df.loc[rows, cols] Label-based selection You know row/column names; inclusive ranges; boolean masks Yes (for slices) Slightly slower than iloc on large data
df.iloc[rows, cols] Position-based selection You know row/column positions; you want Python-style slicing No (exclusive) Fastest

When to choose loc:

  • Your index has meaningful labels (dates, user IDs, categories).
  • You need inclusive slices (e.g., '2024-01-01':'2024-12-31').
  • You're applying a boolean mask and also want specific columns in one go.

When to choose iloc:

  • You want the first N rows (common with df.head(), but df.iloc[:N] is more flexible).
  • You're working with a positional loop or a calculated index.
  • You need the fastest possible selection for a pure positional slice.

Avoid mixing them. If you have labels, use loc; if you have positions, use iloc. Mixing the two in one call will throw an error (e.g., df.loc[0] when index is ['A','B'] raises a KeyError).

Troubleshooting & edge cases

KeyError in loc

If you use loc with a label that doesn't exist in the index, pandas raises KeyError: 'X'. This is a common beginner stumble, especially when the index is a range of integers: df.loc[1] means the row labeled 1, not the second row. Always inspect df.index first.

Off-by-one in iloc slices

iloc uses exclusive stop, just like Python's range. So df.iloc[1:3] returns rows 1 and 2 — not row 3. If you need the first 5 rows, write df.iloc[:5], not df.iloc[1:5].

loc slice inclusivity surprises you

df.loc['A':'C'] includes both 'A' and 'C'. This is intentional, but if you're used to iloc or Python slicing, it catches you off guard. If you want to exclude 'C', you'll have to drop it explicitly (e.g., use a list ['A','B']).

Boolean mask length mismatch

If your condition returns a Series with a different index than your DataFrame, pandas will raise an error. For example, after sorting, filters may not align. Use .reindex() or align the filters to the DataFrame's index:

mask = some_series > 5
mask = mask.reindex(df.index, fill_value=False)
df = df.loc[mask]

SettingWithCopyWarning when using chained indexing

Avoid patterns like df[df['a']>0]['b'] = 5. This may silently fail or trigger warnings. Use loc for assignments:

df.loc[df["price"] > 500, "stock"] = 0  # sets stock to 0 for expensive items

This is the safe and explicit way, and it's a key reason you'll want loc in your arsenal.

What you learned & what's next

You now understand the core difference between loc (label-based) and iloc (position-based), and you can apply both to select columns and rows in a single, readable line. You've learned how to use slices, lists, and boolean masks, and you can choose the right tool for the job. You also know how to troubleshoot the most common errors — key misspellings, slice off-by-one, and mask alignment. Critically, you've seen how loc enables safe assignments without copy warnings.

This directly builds on the previous lesson (loading and inspecting DataFrames) and sets you up for the next one: filtering and sorting data in pandas. You'll combine these selection skills with logic to answer more complex questions — like 'What were the top 5 products by revenue last quarter?' — with elegance and speed.

Keep the analogy in mind: loc is the address, iloc is the map coordinate. Use both consciously, and your data analysis code will be a model of clarity.

Practice recap

Now reinforce these concepts: create your own DataFrame with custom string index labels, then practice selecting rows and columns using both loc and iloc. Try a boolean mask filtering rows where a numeric column exceeds a threshold, and update a value using loc with a condition. Confirm your slices behave as expected — note the inclusive vs exclusive difference — and then move on to the next lesson on filtering and sorting.

Common mistakes

  • Using loc with position integers when the index labels are also integers but not in the same order — df.loc[0] gives the row labeled 0, not necessarily the first row.
  • Forgetting that iloc slices are exclusive of the stop (like Python lists), so df.iloc[1:3] gives rows 1 and 2, not rows 1, 2, and 3.
  • Assuming loc slices are exclusive — they are inclusive of both endpoints, which catches many beginners off guard when they expect a range like ['A':'C'] to exclude 'C'.
  • Chaining selections like df[df['price']>100]['product'] instead of using one loc call — this risks SettingWithCopyWarning and makes the code harder to read and modify.

Variations

  1. You can use df.query('column > 100') for label-based, condition-driven row filtering that is more readable for complex logical expressions.
  2. NumPy-style boolean indexing with df[df['col'] > 0] is a shortcut for row filtering, but it only selects rows — not specific columns — in a separate step.
  3. For high-performance selection on large DataFrames, you can use the pandas .at and .iat accessors to get or set a single scalar value by label or position respectively.

Real-world use cases

  • Extracting customer records from the last year for a cohort analysis using df.loc[df['date'] >= '2024-01-01', ['customer_id', 'revenue']].
  • Pulling the first 1,000 rows of a sensor log for a quick sanity check before cleaning, via df.iloc[:1000].
  • Updating stock levels to zero for discontinued items identified by a condition, using df.loc[df['status'] == 'discontinued', 'stock'] = 0.

Key takeaways

  • loc selects by row/column label and uses inclusive slices; iloc selects by integer position and uses exclusive slices.
  • Both indexers accept single labels, lists, slices, boolean masks, and callables, and can select rows and columns in one call.
  • loc is ideal for meaningful index labels, date ranges, and conditional row filtering combined with column selection.
  • iloc is the fastest choice for positional slicing (e.g., first N rows) and when you know exact column positions.
  • Avoid chained indexing for assignments; use loc with a boolean mask to prevent SettingWithCopyWarning and ensure updates work as expected.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.