Skip to content

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:

# Real code:
df = pd.read_csv("data.csv")

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 columnspd.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 .loc for 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 get 0, 1, 2 column names.

Practice

What does this print?

Expected: (3, 2)

import pandas as pd
records = [{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}]
print(pd.DataFrame(records).shape)

Use 'id' as the row index instead of the default 0/½

Expected: name age

import pandas as pd
df = pd.DataFrame({"id": [101, 102, 103], "name": ["A", "B", "C"], "age": [25, 30, 35]})
print(df)              # bug: id should be the row index, not a column

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 NaN in the corresponding column
  • The row is dropped
  • Defaults to 0

Why: Pandas pads missing keys with NaN. That's why pd.DataFrame([{"a": 1}, {"a": 2, "b": 3}]) produces a 2-column DataFrame with one NaN.

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 id column becomes the row index
  • Sorts the DataFrame by id
  • Renames the column
  • Drops the column

Why: set_index moves a column into the index. Pair with reset_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.

What's next

Reading & Writing Files