One parameter, three personalities: the five-row reflex, the everything-preview, and scissors for the TOTAL row.
One parameter, three personalities: a five-row peek, an everything-preview, and a pair of scissors for the TOTAL row at the bottom of every export.
head(n=5) returns the first n rows of a DataFrame or Series, by position. Three behaviors in one parameter: positive n takes the front, an n bigger than the frame returns everything, and negative n drops the last |n| rows — head(-1) means "all but the last row". It cannot raise: no label matching, no bounds check. On pandas 3.0.3 the whole method is one line: return self.iloc[:n].copy().
head is the reflex I want burned in: after read_csv, after a merge, after sort_values — look first, compute second. Five rows expose wrong dtypes, broken encodings, misaligned columns, and backwards sort direction before they poison a pipeline. The same method hides three tools people otherwise go hunting for: a footer-trimmer for TOTAL rows, an oversized-n safe preview, and groupby.head(n) — the top n rows of every group without a single line of apply().
import pandas as pd
orders = pd.DataFrame({
'order_id': [1001, 1002, 1003, 1004, 1005, 1006, 1007],
'city': ['Berlin', 'Vienna', 'Berlin', 'Zurich', 'Vienna', 'Berlin', 'Zurich'],
'units': [12, 7, 30, 4, 9, 21, 6],
'revenue': [810.0, 55.3, 384.0, 122.0, 96.5, 640.5, 77.25],
})
top = orders.sort_values('revenue', ascending=False)
top.head(3)order_id city units revenue 0 1001 Berlin 12 810.0 5 1006 Berlin 21 640.5 2 1003 Berlin 30 384.0
Verified on pandas 3.0.3. Note the surviving index labels 0, 5, 2 — head keeps original row labels, it does not renumber. sort_values then head is the two-line "who is at the top" audit I run after every report refresh.
# A supplier export ends in a TOTAL row that must go before the merge.
export = pd.DataFrame({
'order_id': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 'TOTAL'],
'city': ['Berlin', 'Vienna', 'Berlin', 'Zurich', 'Vienna', 'Berlin', 'Zurich', '-'],
'units': [12, 7, 30, 4, 9, 21, 6, 89],
'revenue': [810.0, 55.3, 384.0, 122.0, 96.5, 640.5, 77.25, 2185.55],
})
clean = export.head(-1)
print(clean)
print(len(clean))order_id city units revenue 0 1001 Berlin 12 810.00 1 1002 Vienna 7 55.30 2 1003 Berlin 30 384.00 3 1004 Zurich 4 122.00 4 1005 Vienna 9 96.50 5 1006 Berlin 21 640.50 6 1007 Zurich 6 77.25 7
Verified on 3.0.3: head(-1) means all rows except the last — the same convention as GNU head -n -1, and I confirmed it equals export.iloc[:-1] via .equals(). No length math, and the mixed str/int order_id column stops being your problem.
# The two biggest orders per city, original row order kept:
orders.groupby('city').head(2)order_id city units revenue 0 1001 Berlin 12 810.00 1 1002 Vienna 7 55.30 2 1003 Berlin 30 384.00 3 1004 Zurich 4 122.00 4 1005 Vienna 9 96.50 6 1007 Zurich 6 77.25
Verified on 3.0.3. This is groupby.head, not df.head — the docstring promises "a subset of rows with original index and order preserved", and as_index is ignored. Unsorted groups each contribute their first 2 rows in frame order (Berlin keeps 1001 and 1003, not the big ones). For biggest-per-group, sort_values first, then groupby.head(2).
prices = pd.Series(
{'berlin': 810.0, 'vienna': 55.3, 'zurich': 122.0,
'prague': 96.5, 'munich': 640.5, 'linz': 77.25},
name='avg_price',
)
prices.head(3)berlin 810.0 vienna 55.3 zurich 122.0 Name: avg_price, dtype: float64
Same one parameter on Series, name intact. head is one API from a 7-row toy to a 10-million-row parquet read — the preview cost stays zero either way.
| Flag | Meaning |
|---|---|
n (int, default 5) | How many rows to take from the top, positionally — labels play no role, so nothing can fail to match. |
n negative | head(-2) returns all rows except the last two — the coreutils convention, mirrored by tail(-n) for the front. |
n oversized | head(999999) on a 7-row frame returns all 7 rows; head(0) returns an empty frame. Never raises. |
groupby.head(n) | First n rows of each group, original index and order preserved — a positional mask, not apply(lambda g: g.head(n)). |
Series.head(n) | Identical signature on DataFrame, Series, and groupby — the Series name attribute survives the slice. |
df.tail(n) | The sibling: last n rows, same negative convention (tail(-1) drops the first row). Reach for it after sorts and appends. |
I checked the source trees: head exists in pandas v0.4.0 (tag committed September 12, 2011) as a two-liner in pandas/core/frame.py — def head(self, n=5): return self[:n] — and is absent from v0.3.0. tail shipped beside it as return self[-n:], reading exactly like the Python slice idiom they wrapped.
Fifteen years later the docstring still promises "the same behavior as df[:n]". The implementation moved from bare slicing to self.iloc[:n].copy() and the method is now @final on the shared NDFrame base — the contract never changed: positional, front-first, copy out. The negative-n rule mirrors GNU head -n -k, so shell intuition carries over one-to-one.
DataFrame.head is @final on the shared NDFrame base and literally returns self.iloc[:n].copy() — the copy is why edits to a preview never leak into the parent (and under Copy-on-Write, even that copy is deferred until you write). The interesting variant is groupby.head: instead of iterating groups and calling apply, it builds one positional mask via _make_mask_from_positional_indexer(slice(None, n)) and masks the original frame — which is why original index and order survive, and why it stays cheap on wide frames. Same name, two mechanisms, both O(n) slices under the hood.