Creating Series and DataFrames¶
Several ways to build the same DataFrame — pick what fits your data source.
From a dictionary of lists (most common)¶
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol"],
"age": [25, 30, 35],
"city": ["Mumbai", "Delhi", "Pune"],
"salary": [50000, 60000, 75000],
})
print(df)
Keys become column names. Each value is a list — same length for every column.
From a list of dictionaries¶
Each dict = one row.
import pandas as pd
records = [
{"name": "Alice", "age": 25, "city": "Mumbai"},
{"name": "Bob", "age": 30, "city": "Delhi"},
{"name": "Carol", "age": 35}, # missing city → NaN
]
df = pd.DataFrame(records)
print(df)
From a list of lists (rows)¶
import pandas as pd
rows = [
["Alice", 25, "Mumbai"],
["Bob", 30, "Delhi"],
["Carol", 35, "Pune"],
]
df = pd.DataFrame(rows, columns=["name", "age", "city"])
print(df)
From a NumPy array¶
import pandas as pd
import numpy as np
rng = np.random.default_rng(0)
arr = rng.integers(0, 100, size=(5, 4))
df = pd.DataFrame(arr, columns=["math", "science", "english", "history"])
print(df)
Series — a single column¶
import pandas as pd
s = pd.Series([10, 20, 30, 40, 50], name="scores")
print(s)
print()
print("type :", type(s).__name__)
print("dtype:", s.dtype)
print("name :", s.name)
Series with custom index labels¶
By default, the index is 0, 1, 2, .... You can label rows:
import pandas as pd
s = pd.Series(
[25, 30, 35, 40],
index=["Alice", "Bob", "Carol", "Dave"],
name="age",
)
print(s)
print()
print(s["Bob"]) # access by label
print(s["Bob":"Dave"]) # slice — INCLUSIVE on both ends
DataFrame with a custom row index¶
import pandas as pd
df = pd.DataFrame({
"age": [25, 30, 35],
"salary": [50000, 60000, 75000],
}, index=["alice", "bob", "carol"])
print(df)
print()
print(df.loc["bob"]) # access a row by label
Creating an empty DataFrame¶
import pandas as pd
df = pd.DataFrame(columns=["name", "age", "city"])
print(df)
print("shape:", df.shape)
# Add rows one at a time (slow — better to collect a list and build once)
df.loc[len(df)] = ["Alice", 25, "Mumbai"]
df.loc[len(df)] = ["Bob", 30, "Delhi"]
print(df)
For real apps, don't append rows one by one — it's slow. Build a list of dicts and create the DataFrame once.
From a range — useful for examples¶
import pandas as pd
df = pd.DataFrame({
"x": range(10),
"y": [v * v for v in range(10)],
})
print(df)
Pre-built example data¶
Use pd.date_range, pd.Categorical, or NumPy random to fake datasets fast:
import pandas as pd
import numpy as np
rng = np.random.default_rng(0)
n = 8
df = pd.DataFrame({
"date": pd.date_range("2025-01-01", periods=n, freq="D"),
"sales": rng.integers(100, 500, size=n),
"region": rng.choice(["North", "South", "East", "West"], size=n),
"rating": rng.uniform(1, 5, size=n).round(1),
})
print(df)
Setting an index after creation¶
import pandas as pd
df = pd.DataFrame({
"id": [101, 102, 103],
"name": ["Alice", "Bob", "Carol"],
"age": [25, 30, 35],
})
# Make 'id' the row index
df = df.set_index("id")
print(df)
print()
# Reset back to default integer index
df = df.reset_index()
print(df)
Inspecting what you just created¶
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol", "Dave", "Eve"],
"age": [25, 30, 35, 40, 45],
"salary": [50000, 60000, 75000, 90000, 100000],
})
print("shape :", df.shape) # (rows, cols)
print("columns :", df.columns.tolist())
print("dtypes :")
print(df.dtypes)
print()
print("first 3 rows:")
print(df.head(3))
print()
print("last 2 rows:")
print(df.tail(2))
Reading from a CSV — quick teaser¶
The most common way to create a DataFrame in real life:
We dedicate the next chapter to file I/O.
Cheatsheet¶
| From | Use |
|---|---|
| Dict of lists | pd.DataFrame({"col": [...]}) |
| List of dicts (rows) | pd.DataFrame(records) |
| List of lists | pd.DataFrame(rows, columns=[...]) |
| NumPy array | pd.DataFrame(arr, columns=[...]) |
| Empty | pd.DataFrame(columns=[...]) |
| CSV | pd.read_csv("path") |
| Excel | pd.read_excel("path") |
| JSON | pd.read_json("path") |
| SQL | pd.read_sql(query, conn) |
| Parquet | pd.read_parquet("path") |
Common pitfalls¶
- ❗ Mixed-length columns —
pd.DataFrame({"a": [1,2], "b": [3,4,5]})raises. All columns must have the same length. - ❗ Modifying a slice that's actually a copy — Pandas sometimes warns
SettingWithCopyWarning. Use.locfor clean assignments. - ❗ Appending rows in a loop — slow. Collect into a list, create the DataFrame once at the end.
- ❗ Forgetting
columns=when creating from a list of lists — you get0, 1, 2column names.
Practice¶
What does this print?
Expected: (3, 2)
Use 'id' as the row index instead of the default 0/½
Expected: name age
Quiz — Quick check¶
What you remember
Q1. Building a DataFrame from a list of dicts (records). What happens to rows missing a key?
- Raises
ValueError - Missing values become
NaNin the corresponding column - The row is dropped
- Defaults to 0
Why: Pandas pads missing keys with
NaN. That's whypd.DataFrame([{"a": 1}, {"a": 2, "b": 3}])produces a 2-column DataFrame with oneNaN.
Q2. Why is appending rows in a loop discouraged?
- It changes the dtype
- Each append allocates a new buffer and copies — O(N²) for N appends
- Pandas doesn't support it
- It breaks the index
Why: DataFrames have a fixed memory layout. Build a list of records or dicts, then call
pd.DataFrame(list)once at the end.
Q3. What does df.set_index("id") do?
- Returns a new DataFrame where the
idcolumn becomes the row index - Sorts the DataFrame by
id - Renames the column
- Drops the column
Why:
set_indexmoves a column into the index. Pair withreset_index()to do the reverse.
Common doubts¶
Should I always set an index?
Not always. Default integer indices are fine for most operations. Set a custom index when you'll look up rows by a meaningful label (user ID, date, ticker symbol), or when you'll merge with another DataFrame on that key.
What's dtype: object and should I worry?
object means "Python objects" — usually strings, but possibly a mix of types. It's not space-efficient. For pure-string columns, convert with df["c"] = df["c"].astype("string") (Pandas 1.0+). For low-cardinality categories, use astype("category") — much smaller in memory.
Why does pd.DataFrame(some_dict) sometimes give weird shapes?
Pandas tries to infer the layout. If your dict values aren't equal-length lists, it raises. If they're scalars, you might need to pass index=[...] so Pandas knows it's a single-row DataFrame.