pandas head() — preview the first rows and trim the footer with the same call

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.

What it does

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().

Why it matters

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().

Examples

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.

Flags

FlagMeaning
n (int, default 5)How many rows to take from the top, positionally — labels play no role, so nothing can fail to match.
n negativehead(-2) returns all rows except the last two — the coreutils convention, mirrored by tail(-n) for the front.
n oversizedhead(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.

In with the slice syntax (2011)

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.

Still df[:n], just spelled out

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.

Under the hood: a mask, not a loop

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.

Fun facts

Pros

Cons

Takeaways