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.
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.
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.
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.
| Flag | Meaning |
|---|---|
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 |
pandas/core/frame.py with the DataFrame class was in Wes McKinney's very first SVN-import commit, "first commit with cleaned up code", dated August 5, 2009 (git-svn revision @5 on Google Code, four months before 0.1.0 hit PyPI on December 25). Back then the signature was just data, index, columns — no dtype, no copy — and every column was a Series internally.
The 2009 class already carried classmethod helpers: fromDict (a two-level {col: {idx: val}} tree — the ancestor of today's from_dict) and from_records. 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.
Everything lands in pandas.core.frame.DataFrame.__init__, which dispatches on type: dict paths go through _from_dict (dicts of Series get union-aligned index arithmetic, tuple keys build a MultiIndex), ndarray/list paths through _from_arrays or _from_records, and a passed DataFrame re-uses its manager via copy_or_deepcopy semantics. Since 2.x the result is wrapped in a DataFrameManager 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.