pandas Series() — the one-dimensional column everything else is built from

A DataFrame is just a dict of Series in a trench coat — learn the Series and the DataFrame stops being mysterious.

Every column you have ever filtered in pandas is a Series, every row a method hands you is a Series — and arithmetic between two of them aligns by label before it computes, which explains half the NaNs you have ever seen.

What it does

pd.Series(data, index, dtype, name) builds a one-dimensional labeled array — NumPy's array plus an index and an optional name. A list gives you a default 0..n-1 index; a dict turns keys into index labels; index= overrides or supplies the labels; name= attaches the label that travels with the object (df['col'] gets its column name this way). Two Series meeting in an operator align on the index first, compute second — mismatched labels produce NaN, not an error. And it cuts both ways: a DataFrame column is a Series, but so is any single row you pull with .loc.

Why it matters

Every pandas skill you already have — filtering with between() and isin(), cleaning with fillna(), aggregating with sum() — is really a Series skill: df['col'] hands you one, the operation runs over it, and the result is usually another Series you feed onward. Understanding the Series is what makes index alignment click: q1 + q2 over cities matches Berlin to Berlin and puts NaN wherever a label exists on one side only, which is exactly the union-outer-join behavior the DataFrame constructor inherited. Once you think in Series, the DataFrame's quirks stop being quirks.

Examples

import pandas as pd

# a dict builds a Series: keys become the index, the name is up to you
prices = pd.Series({"berlin": 810, "vienna": 640, "zurich": 590})
print(prices)
print(prices.index.tolist(), prices.name)
berlin    810
vienna    640
zurich    590
dtype: int64
['berlin', 'vienna', 'zurich'] None

Dict input maps keys to index labels. There is no name unless you pass name= — a Series is one labeled column, not a table.

q1 = pd.Series({"berlin": 120, "vienna": 85, "zurich": 95})
q2 = pd.Series({"berlin": 110, "vienna": 90, "milano": 40})
print(q1 + q2)
berlin    230.0
milano      NaN
vienna    175.0
zurich      NaN
dtype: float64

The alignment rule that explains a thousand mystery NaNs: pandas unions the two indexes and computes only where both sides have the label. milano and zurich exist on one side each, so they get NaN — and int64 upcasts to float64 the moment NaN appears.

sales = pd.DataFrame({
    "order_id": ["A-1042", "A-1043", "A-1044", "A-1045", "A-1046"],
    "city": ["Berlin", "Vienna", "Berlin", "Zurich", "Berlin"],
    "amount_eur": [89.50, 42.00, 130.25, 61.75, 15.00],
})
s = sales["amount_eur"]
print(type(s), s.name)
print((s > 50).sum())
print(s.sum())
<class 'pandas.core.series.Series'> amount_eur
3
338.5

A DataFrame column IS a Series — same object type, and it carries its column name along, which is why filtered columns keep a useful label. Mask it, sum it, feed it to anything: the whole one-dimensional toolkit applies.

row = sales.loc[2]
print(row)
print(row["city"], row["amount_eur"])
order_id      A-1044
city          Berlin
amount_eur    130.25
Name: 2, dtype: object
Berlin 130.25

Rows are Series too — the column names become the index and the row label becomes the name. One data structure, two orientations, which is why so many pandas operations work identically on rows and columns.

a = pd.Series([10, 20], name="revenue_2026")
print((a * 2).name)
print((a + pd.Series([1, 2], name="cost_2026")).name)
revenue_2026
None

The name attribute survives unary arithmetic but vanishes the moment two differently-named Series meet — pandas refuses to guess which label the result deserves. Useful as a quick 'which columns fed this?' probe in notebooks.

Flags

FlagMeaning
pd.Series([1, 2, 3])list input: values in, default RangeIndex 0..n-1
pd.Series({'a': 1})dict input: keys become index labels, values become the data
index=['x', 'y']explicit labels; also accepts a DatetimeIndex from pd.date_range for time series
name='amount_eur'label attached to the Series — becomes the column name when placed in a DataFrame
dtype='float64'force a dtype at construction instead of letting inference decide
s1 + s2arithmetic aligns on index first — missing labels become NaN, never an error
df['col'] / df.loc[row]both return Series: columns by name, rows as transposed label->value pairs

There from the first commit (August 2009)

The Series class predates the 0.1.0 release: it sits in Wes McKinney's original SVN import of pandas on Google Code, alongside the first DataFrame, and appears in the 0.1.0 docs of December 2009 as the library's core 1-D structure. The original class already carried what still defines it — a data array, an index, and arithmetic that aligns on labels rather than positions.

Design note: alignment is the feature

Series inherited its index-aligned arithmetic straight from R's data frames and from AQR's internal tooling, where Wes McKinney built pandas as a quant analyst. Aligning labels before computing was the deliberate break from NumPy, which aligns by position. That decision explains both the NaN behavior in mixed indexes and the int-to-float upcast — and it is why reindexing, joining, and time-series resampling all feel native in pandas rather than bolted on.

Under the hood: one array, one index

A Series wraps two NumPy arrays: the values block and the Index object that labels it. Element-wise ops ride NumPy directly on the values; the label work happens before and after — Index.union() to merge two indexes, take-based reindexing to line values up with the union, then the arithmetic. That is why the aligned add costs roughly a hash-union plus a gather, and why a same-index op skips straight to the NumPy fast path. Under pandas 3's Copy-on-Write, slicing a Series is a lazy view that copies only on first write — so s = df['col'] stays cheap until you actually write to it.

Fun facts

Pros

Cons

Takeaways