kmail.at
contact

elsewhere

EnglishEN
← learning

pandas · difficulty ◆◆

pandas DataFrame() — build a table from dicts, lists, arrays, and Series

Every pandas session starts with the same five parameters: data, index, columns, dtype, copy — learn what each one does to your data.

The pandas constructor predates the 0.1 release, had just three parameters, and its docstring warned that data is COPIED — in all caps.

AI-generated draft · reviewed and editedThis text was generated with AI as a first draft, then reviewed and edited by the author, who holds editorial responsibility for it.

2026-09-10 · 5 min read

$ pd.DataFrame()
5
Constructor params

data, index, columns, dtype, copy — verified signature on pandas 3.0.3 (the 2009 class took only data and index)

2009
In pandas since

pandas/core/frame.py in the first commit, 2009-08-05; 0.1.0 released 2009-12-25

10+
Input shapes handled

dict-of-lists, list-of-dicts, 2D/record ndarray, dict/list of Series, scalar, tuple-keyed dict, another DataFrame...

What it does

pd.DataFrame(data, index, columns, dtype, copy) turns almost any tabular-ish Python object into a DataFrame. The shape of `data` decides the layout: a list of dicts becomes one row per dict (missing keys become NaN), a dict of lists or Series becomes one column per key, a 2D NumPy array needs columns= to get names, and a dict with tuple keys builds MultiIndex columns for free. index and columns override or supply the labels; dtype forces one dtype across everything (ints become floats); copy=False asks pandas to share memory with the input instead of duplicating it.

Why it matters

Everything else in pandas — query, merge, groupby — operates on a DataFrame, so the constructor is the one API you cannot skip. In real work it shows up as: assembling a report from rows pulled off an API (list of dicts), aligning two sensors that logged different hours (dict of Series, which union-aligns the index), giving a NumPy matrix from legacy code real column names, and hard-typing columns before a Parquet write so downstream schemas stay stable. Get the constructor's rules right once and every ingestion path after it behaves predictably.

Example

$ A customer API returns orders as JSON rows. The natural ingestion is a list of dicts — one dict per row, keys become columns:
   order_id    city  revenue
0      1001  Berlin    144.0
1      1002  Vienna     55.3
2      1003  Berlin    384.0
order_id      int64
city            str
revenue     float64
dtype: object

pd.DataFrame([{'order_id': 1001, 'city': 'Berlin', 'revenue': 144.0}, ...]) with RangeIndex 0..2. Note the dtypes: pandas 3.0 gives text columns the new 'str' dtype, and a record that omits a key (say Vienna has no revenue) silently becomes NaN — the constructor never guesses.

$ Two machine sensors logged different hours; you want one aligned table anyway. A dict of Series union-aligns the index and fills the gaps:
        price  units
berlin  810.0   12.0
vienna  640.0    NaN
zurich    NaN    9.0

pd.DataFrame({'price': s2, 'units': s1}) — vienna has no units reading and berlin has no price reading, so NaN appears exactly where data is missing. This alignment is a feature, not a bug: it is the same rule SQL FULL OUTER JOIN uses, built into construction. Feed a named index for readable row labels: pd.DataFrame({'temp_c': [...], 'rpm': [...]}, index=pd.Index(['mon','tue','wed'], name='day')).

$ Legacy code hands you a bare NumPy matrix plus you need forced column names — or you want a column order you choose yourself:
    x  y  z
r1  1  2  3
r2  4  5  6

pd.DataFrame(np.array([[1,2,3],[4,5,6]]), index=['r1','r2'], columns=['x','y','z']) — with 2D arrays, columns= is how the matrix gets names. Verified also: pd.DataFrame(rec) on a structured/record array adopts the field names as columns automatically (order_id, city, revenue), and pd.DataFrame({'temp_c': [21.5, 22.1], 'rpm': [1420, 1390]}, dtype='float64') upcasts the int rpm column to float — passing dtype forces every column.

Common flags

data
ndarray, dict of arrays/Series, list of dicts, list of lists, another DataFrame — the input shape sets the layout
index=...
row labels; supply with ndarray/list data, or conform/override existing index
columns=...
column names; required (or at least useful) for bare arrays, reorderable for dict input
dtype='float64'
force one dtype everywhere; int columns upcast, strings stay strings
copy=False
share input data instead of copying — safe under pandas 3.0's Copy-on-Write, but writes to either object then trigger a copy
list of Series
pd.DataFrame([s1, s2]) treats each Series as a ROW; the dict form {'a': s1} treats each Series as a column — the most common constructor mix-up
scalar broadcast
pd.DataFrame({'city': ['Berlin', 'Vienna'], 'country': 'DE'}) — a scalar fills the whole column

History

Day one of the project (2009)

pandas/core/frame.py with the DataFrame class was in the commit "first commit with cleaned up code", dated August 5, 2009 (git-svn revision @5 on Google Code, the first commit that carried any code, four months before 0.1.0 hit PyPI on December 25). Back then the signature was just data and index — no columns, no dtype, no copy — and every column was a Series internally, held in a self._series dict.

The alternate-constructor family

The 2009 class already carried classmethod helpers: fromDict (a two-level {col: {idx: val}} tree, its docstring spelling out tree['returns'][curDate] — the ancestor of today's from_dict) and fromMatrix (an ndarray plus column and row names). They were added because the bare constructor couldn't disambiguate every input shape; fifteen years later pd.DataFrame.from_dict(..., orient='index') is still the sanctioned way to build row-keyed data.

Under the hood

Under the hood: one funnel, five doors

Everything lands in pandas.core.frame.DataFrame.__init__, which resolves copy=None first (True for dict data and for anything that isn't an Index, Series or DataFrame; False for those three), then dispatches on type: dict paths go through dict_to_mgr — dicts of Series get union-aligned index arithmetic, tuple keys build a MultiIndex — while ndarrays go through ndarray_to_mgr, structured arrays through rec_array_to_mgr, and a named Series or Index is turned back into a dict and routed through dict_to_mgr again. A DataFrame passed as data simply re-uses its manager, shallow-copied. The construction helpers live in pandas.core.internals.construction and the result is wrapped in a BlockManager of column blocks; under Copy-on-Write (default since 3.0) copy=False can hand you a zero-copy view that only materializes a copy on first write — which is why the old all-caps COPIED warning is gone from the docstring.

Fun facts

Pros & cons

pros

  • + One entry point for nearly every data shape Python can hand you — records, columns, matrices, Series
  • + Dict-of-Series input gets index union-alignment for free, like a built-in outer join on row labels
  • + index/columns/dtype let you fix names, order, and types at birth, before any downstream code runs

cons

  • − The list-of-Series vs dict-of-Series asymmetry silently transposes tables and bites nearly everyone once
  • − Unequal-length arrays inside one dict raise ValueError('All arrays must be of the same length') — padding is your job (a scalar in the same dict broadcasts, and a ragged list of lists is silently padded with NaN instead)
  • − Auto dtype inference can surprise: mixed int/float becomes float64, text becomes 'str' in 3.0 — pin dtype= when it matters

Takeaways

  1. 1Records on hand? list-of-dicts. Columns on hand? dict-of-lists/Series — the input shape IS the layout
  2. 2Dict of Series union-aligns row labels and NaN-fills gaps — a free outer join at construction time
  3. 3pd.DataFrame([s1, s2]) is rows, pd.DataFrame({'a': s1}) is columns — the classic transposition trap
  4. 4Pass columns= to name and order array-based data in one go; pass dtype= to stop inference surprises
  5. 5copy=False gives a CoW-safe shallow frame in pandas 3.0 — cheap to build, copy deferred until first write

Related commands