Skip to main content

How to Create a Pandas DataFrame from a List of Lists

Lists of lists are a natural format for row-oriented data in Python. Each inner list represents one complete record, making this structure common when reading parsed files, collecting results in loops, or receiving data from functions that return rows. Pandas converts this format directly into a DataFrame, with each inner list becoming a row in the resulting table.

In this guide, you will learn how to create DataFrames from lists of lists, add column names and custom indices, handle missing data and unequal row lengths, and deal with column-oriented lists.

Standard Row-Wise Conversion

Pass the list of lists to pd.DataFrame() and specify column names with the columns parameter:

import pandas as pd

data = [
['Apple', 999, 10],
['Samsung', 899, 20],
['Google', 799, 15]
]

df = pd.DataFrame(data, columns=['Brand', 'Price', 'Qty'])

print(df)

Output:

     Brand  Price  Qty
0 Apple 999 10
1 Samsung 899 20
2 Google 799 15

Each inner list maps to one row, and the columns parameter assigns meaningful names instead of the default numeric headers (0, 1, 2).

Adding a Custom Index

Replace the default sequential index with custom labels using the index parameter:

import pandas as pd

data = [
['Alice', 88],
['Bob', 92],
['Charlie', 78]
]

df = pd.DataFrame(
data,
columns=['Name', 'Score'],
index=['s1', 's2', 's3']
)

print(df)

Output:

       Name  Score
s1 Alice 88
s2 Bob 92
s3 Charlie 78

Custom indices let you access rows by meaningful labels (e.g., df.loc['s1']) rather than by position.

Handling Missing Data

When inner lists contain None, Pandas automatically converts them to NaN:

import pandas as pd

data = [
['Project A', 5000],
['Project B', None],
['Project C', 3000]
]

df = pd.DataFrame(data, columns=['Name', 'Budget'])

print(df)
print()
print(df.dtypes)

Output:

        Name  Budget
0 Project A 5000.0
1 Project B NaN
2 Project C 3000.0

Name object
Budget float64
dtype: object
note

Notice that the Budget column became float64 even though the original values were integers. This happens because standard NumPy integers cannot represent NaN, so Pandas automatically upcasts the column to float. If you need integer behavior with missing values, use the nullable integer type:

import pandas as pd

data = [
['Project A', 5000],
['Project B', None],
['Project C', 3000]
]

df = pd.DataFrame(data, columns=['Name', 'Budget'])

df['Budget'] = df['Budget'].astype('Int64') # Capital I for nullable integer
print(df['Budget'])

Output:

0    5000
1 <NA>
2 3000
Name: Budget, dtype: Int64

Transposing When Lists Represent Columns

Sometimes each inner list represents a column's values rather than a row's values. Use .T (transpose) to swap rows and columns:

import pandas as pd

# Each inner list contains one column's data
data = [
[100, 200, 300], # Sales values
[10, 20, 30] # Units values
]

df = pd.DataFrame(data).T
df.columns = ['Sales', 'Units']

print(df)

Output:

   Sales  Units
0 100 10
1 200 20
2 300 30

Without the transpose, you would get 2 rows and 3 columns instead of the intended 3 rows and 2 columns.

Handling Ragged Lists (Unequal Lengths)

When inner lists have different numbers of elements, Pandas pads shorter rows with None to create a rectangular DataFrame:

import pandas as pd

data = [
['A', 1, 2],
['B', 3], # Missing one element
['C', 4, 5, 6] # One extra element
]

df = pd.DataFrame(data)

print(df)

Output:

   0  1    2    3
0 A 1 2.0 NaN
1 B 3 NaN NaN
2 C 4 5.0 6.0

The resulting DataFrame has 4 columns to accommodate the longest list. Shorter lists have NaN or None in the trailing positions.

warning

Ragged lists often indicate a data quality issue. While Pandas handles them without raising an error, the automatic padding can mask problems in your data pipeline. If all rows should have the same number of elements, consider validating the data before creating the DataFrame:

expected_length = 3
for i, row in enumerate(data):
if len(row) != expected_length:
print(f"Row {i} has {len(row)} elements, expected {expected_length}")

Specifying Data Types After Creation

Pandas infers data types automatically, but you often need to convert columns to more appropriate types:

import pandas as pd

data = [
['2024-01-01', 100],
['2024-01-02', 200]
]

df = pd.DataFrame(data, columns=['Date', 'Value'])

# Convert types
df['Date'] = pd.to_datetime(df['Date'])
df['Value'] = df['Value'].astype('Int64')

print(df.dtypes)

Output:

Date     datetime64[ns]
Value Int64
dtype: object

Practical Example: Building a DataFrame from Computed Results

A common pattern is generating rows in a loop and collecting them as a list of lists:

import pandas as pd

# Simulate computing results row by row
results = []
for i in range(1, 6):
name = f"Item_{i}"
price = i * 10.5
in_stock = i % 2 == 0
results.append([name, price, in_stock])

df = pd.DataFrame(results, columns=['Product', 'Price', 'In_Stock'])

print(df)

Output:

  Product  Price  In_Stock
0 Item_1 10.5 False
1 Item_2 21.0 True
2 Item_3 31.5 False
3 Item_4 42.0 True
4 Item_5 52.5 False
tip

While list of lists works well for collecting results in loops, list of dictionaries is often more readable because each field is labeled explicitly. Choose list of lists when performance matters or when the column structure is fixed and well-known. Choose list of dictionaries when clarity and self-documentation are more important.

Quick Reference

GoalMethod
Standard creationpd.DataFrame(data, columns=['X', 'Y'])
With custom indexpd.DataFrame(data, columns=[...], index=[...])
Transposepd.DataFrame(data).T
Handle NoneAutomatic conversion to NaN
Nullable integers.astype('Int64') after creation
  • Lists of lists are ideal for row-by-row data such as parsed logs, query results, or computed records.
  • Always specify the columns parameter for immediate readability.
  • Use .T if your inner lists represent columns rather than rows.
  • Be aware that None values convert to NaN and may cause integer columns to become floats unless you use the nullable Int64 type.