Selecting Columns
Pick one column (a Series) or several columns (a DataFrame) with df['col'] and df[['a', 'b']], and learn why attribute access df.col is risky.
Most questions about a dataset are about some of its columns: "show me just the players and their runs", "what is the average strike rate?". Selecting columns is the very first indexing skill in pandas — and the difference between single and double square brackets confuses almost every beginner.
All topics in this chapter use the same small IPL squad table:
python
import pandas as pd
players = pd.DataFrame({
"player": ["Virat Kohli", "Rohit Sharma", "MS Dhoni", "Jasprit Bumrah",
"Hardik Pandya", "Ravindra Jadeja", "Shubman Gill", "Rashid Khan"],
"team": ["RCB", "MI", "CSK", "MI", "MI", "CSK", "GT", "GT"],
"role": ["Batter", "Batter", "Keeper", "Bowler",
"All-rounder", "All-rounder", "Batter", "Bowler"],
"runs": [741, 417, 161, 8, 216, 267, 426, 102],
"wickets": [0, 0, 0, 20, 11, 8, 0, 10],
"strike_rate": [154.7, 150.0, 220.5, 80.0, 143.0, 157.1, 147.4, 120.0],
})
print(players)text
player team role runs wickets strike_rate
0 Virat Kohli RCB Batter 741 0 154.7
1 Rohit Sharma MI Batter 417 0 150.0
2 MS Dhoni CSK Keeper 161 0 220.5
3 Jasprit Bumrah MI Bowler 8 20 80.0
4 Hardik Pandya MI All-rounder 216 11 143.0
5 Ravindra Jadeja CSK All-rounder 267 8 157.1
6 Shubman Gill GT Batter 426 0 147.4
7 Rashid Khan GT Bowler 102 10 120.0Definition
Column selection uses the indexing operator
[]on a DataFrame with a column label: a single label returns that column as a Series, while a list of labels returns a new DataFrame containing those columns in the order listed.
- Indexing operator
[]— the square brackets right after the DataFrame name:players[...]. - Label — the column's name exactly as stored (case-sensitive:
"runs"is not"Runs"). - Series — a one-dimensional labelled array: one column plus the row index.
- List of labels — the inner brackets build a Python list; the outer brackets do the selecting. That is why it looks like
[[ ]].
Syntax
python
df["col"] # one column -> Series
df[["col1", "col2"]] # many columns -> DataFrame (in this order)
df[["col"]] # one column but kept as a DataFrame
df.col # attribute access -> Series (convenient but risky)
df.get("col", default) # like df["col"] but returns default if missing| Form | Input | Returns |
|---|---|---|
df["runs"] | one label (string) | Series |
df[["player", "runs"]] | list of labels | DataFrame |
df[["runs"]] | list with one label | DataFrame (one column) |
df.runs | attribute name | Series |
df.get("salary", default) | label + fallback value | Series, or the default if the column doesn't exist |
Example — single vs double brackets
python
s = players["runs"]
print(type(s))
print(s)
d = players[["player", "runs"]]
print(type(d))
print(d)text
<class 'pandas.Series'>
0 741
1 417
2 161
3 8
4 216
5 267
6 426
7 102
Name: runs, dtype: int64
<class 'pandas.DataFrame'>
player runs
0 Virat Kohli 741
1 Rohit Sharma 417
2 MS Dhoni 161
3 Jasprit Bumrah 8
4 Hardik Pandya 216
5 Ravindra Jadeja 267
6 Shubman Gill 426
7 Rashid Khan 102How it works: the Series prints as two columns of text — the index on the left and the values on the right — followed by its Name (the column label) and dtype. The DataFrame prints as a table with a header row. Use a Series when you want to calculate on one column (players["runs"].mean()); use a DataFrame when you want to show or keep a sub-table. Columns come back in the order you list them: players[["runs", "player"]] puts runs first.
Example — attribute access and its pitfalls
players.runs gives the same Series as players["runs"], and it's quick to type. But it breaks in several situations:
python
x = pd.DataFrame({"count": [1, 2], "size": [3, 4], "team name": ["a", "b"]})
print(x.count) # NOT the column - it's the DataFrame.count method!
print(x["count"]) # the column
print(x.size) # NOT the column - the number of cells (rows x columns)text
<bound method DataFrame.count of count size team name
0 1 3 a
1 2 4 b>
0 1
1 2
Name: count, dtype: int64
6How it works: count and size are already names of DataFrame methods/attributes, so x.count returns the method and x.size returns the number of cells (2 rows × 3 columns = 6), not your columns. A column with a space (team name) or one starting with a digit (2024_runs) can't be written as an attribute at all. And creating a column via an attribute silently does nothing useful:
python
players.new_col = 5
print("new_col" in players.columns) # False - it became a Python attribute, not a column
players["new_col"] = 5
print(players.columns.tolist())text
False
['player', 'team', 'role', 'runs', 'wickets', 'strike_rate', 'new_col']Rule of thumb: use df["col"] in real code. Attribute access is fine for quick interactive peeking at simple names.
More ways to pick columns
| Goal | Code |
|---|---|
| Columns from a Python list variable | cols = ["player", "team"]; players[cols] |
| A range of columns by name | players.loc[:, "team":"runs"] (next topic) |
| Columns by position | players.iloc[:, 0:3] |
| Only numeric / only text columns | players.select_dtypes(include="number") / include="str" |
| Names containing some text | players.filter(like="r") |
| Column labels themselves | players.columns, players.columns.tolist() |
Common mistakes
- ❌
players["player", "runs"]→KeyError: ('player', 'runs')(pandas looks for one column named by that tuple). ✅players[["player", "runs"]]— a list inside the brackets. - ❌
players["Runs"]→KeyError: 'Runs'. ✅ Labels are case-sensitive; checkplayers.columns. Watch out for hidden spaces like"runs ". - ❌
players.count,players.size,players.shapeexpecting columns. ✅ Use brackets for any column whose name could clash with a method. - ❌
players.new_col = 5to create a column. ✅players["new_col"] = 5.
Comparison — Series vs one-column DataFrame
players["runs"] | players[["runs"]] | |
|---|---|---|
| Type | Series | DataFrame |
| Dimensions | 1 (ndim = 1) | 2 (ndim = 2) |
| Shape | (8,) | (8, 1) |
| Prints | index + values + Name: runs | table with a runs header |
| Typical use | math: .mean(), .max(), comparisons | keep a table shape, join, export |
Interview tip
Q: What is the difference between df["a"] and df[["a"]]?
A: df["a"] returns a 1-D Series; df[["a"]] passes a list, so it returns a 2-D DataFrame with one column. Methods that need a DataFrame (like merge or most scikit-learn feature inputs) expect the double-bracket form.
Key points to remember
- df['col'] returns one column as a Series; df[['a', 'b']] returns a DataFrame with those columns.
- Double brackets are just a Python list inside the indexing operator — the list order sets the column order.
- df[['col']] keeps a single column as a DataFrame (shape (n, 1)) instead of a Series (shape (n,)).
- Column labels are case-sensitive; a wrong name raises KeyError.
- Attribute access (df.col) fails for names with spaces, names that clash with methods (count, size), and for creating columns.
- select_dtypes(include='number' / 'str') and filter(like=...) pick columns by type or name pattern.
Try it yourself
Series vs DataFrame selection
Attribute access pitfalls
Picking columns by list, type and pattern
Practice: build a batting card
Revision Flashcards
Tap a card to flip it and see the answer.
