head shows the front door; tail checks the back room where appends, footers, and stale readings hide.
head(5) shows you the front door of a dataset. tail(5) is the walk through the back room — where appends land, where TOTAL footers live, and where yesterday's bad merge is still smoldering.
tail(n=5) returns the last n rows of a DataFrame or Series, positionally — labels play no role, so nothing can fail to match. Three behaviors in one parameter: positive n takes from the back, an oversized n returns the whole frame, and negative n drops the FIRST |n| rows — tail(-2) is "all but the first two", the mirror of head(-2). The docstring spells out its own use case: verifying data after sorting or appending rows. On pandas 3.0.3 the implementation is two lines: return self.iloc[-n:].copy().
Bad data hides at the bottom: TOTAL footers from supplier exports, appends that double-counted a row, merges that shifted the index into the weeds, timestamp streams where the newest reading is the one that's broken. tail is the cheap audit — after any append, merge, or sort_values, look at the end before you aggregate. The same method hides two tools people go hunting for elsewhere: a header-rescuer for mis-read files, and groupby.tail(n) — the n most recent rows of every group in original frame order, no apply() required.
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],
})
late = pd.DataFrame({
'order_id': [1008, 1009],
'city': ['Prague', 'Munich'],
'units': [15, 5],
'revenue': [512.0, 44.0],
})
combined = pd.concat([orders, late], ignore_index=True)
combined.tail(4)order_id city units revenue 5 1006 Berlin 21 640.50 6 1007 Zurich 6 77.25 7 1008 Prague 15 512.00 8 1009 Munich 5 44.00
Verified on pandas 3.0.3. Exactly the case the docstring names — verify after appending rows: the two late orders sit at the bottom, original index labels intact (tail slices positionally, it never renumbers). I confirmed combined.tail(4).equals(combined.iloc[-4:]) is True.
# A report where the header landed as data rows (read_csv with header=None gone wrong).
messy = pd.DataFrame({
'city': ['Report: Sales', 'generated 2026-09-23', 'Berlin', 'Vienna', 'Zurich', 'Munich'],
'units': [-999, -999, 12, 7, 4, 5],
})
messy.tail(-2).reset_index(drop=True)city units 0 Berlin 12 1 Vienna 7 2 Zurich 4 3 Munich 5
Verified on 3.0.3: tail(-2) means all rows except the first two — GNU coreutils convention mirrored, no length math needed. Verified tail(-2).equals(df.iloc[2:]) is True. The reset_index(drop=True) is cosmetic: tail keeps original labels (2, 3, 4, 5 here), it doesn't renumber.
# The two most recent readings per sensor, in one line:
readings = pd.DataFrame({
'sensor': ['S1','S1','S1','S1','S2','S2','S2','S2','S3','S3','S3','S3'],
'ts': ['09:00','09:15','09:30','09:45','09:00','09:15','09:30','09:45','09:00','09:15','09:30','09:45'],
'temp': [21.4, 21.9, 22.3, 23.1, 18.2, 18.0, 17.8, 17.5, 24.0, 24.6, 25.1, 26.2],
})
readings.sort_values(['sensor', 'ts']).groupby('sensor').tail(2)sensor ts temp 2 S1 09:30 22.3 3 S1 09:45 23.1 6 S2 09:30 17.8 7 S2 09:45 17.5 10 S3 09:30 25.1 11 S3 09:45 26.2
groupby.tail, not df.tail — the docstring promises "a subset of rows with original index and order preserved", as_index is ignored. This is the latest-reading-per-sensor report without a single line of apply(). For the oldest, groupby('sensor').head(2); for the newest single value per group, groupby('sensor').last().
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.tail(3)prague 96.50 munich 640.50 linz 77.25 Name: avg_price, dtype: float64
Same one parameter on Series, name attribute intact. Verified on 3.0.3: tail(999999) on a 9-row frame returns all 9 rows, tail(0) is empty — it never raises. On pandas 3.0.3 the whole method is return self.iloc[-n:].copy().
| Flag | Meaning |
|---|---|
n (int, default 5) | How many rows to take from the bottom, positionally — the label at row 10,000 can't break it, unlike a loc-based peek. |
n negative | tail(-2) returns all rows except the FIRST two — the coreutils convention, mirrored by head(-n) for the bottom. |
n oversized | tail(999999) on a 9-row frame returns all 9 rows; tail(0) is empty. Never raises — safe anywhere after any load. |
groupby.tail(n) | Last n rows of each group, original index and order preserved — a positional mask, not apply(lambda g: g.tail(n)). |
Series.tail(n) | Identical signature on DataFrame, Series, and groupby — the Series name attribute survives the slice. |
df.head(n) | The sibling: first n rows, same negative convention (head(-1) drops the last row). Reach for it after sorts and loads. |
I checked the source trees: tail exists in pandas v0.4.0 (tag committed September 12, 2011) as a two-liner in pandas/core/frame.py — def tail(self, n=5): return self[-n:] — and the v0.4.0 tag is the oldest release tag on the repo, so I can't push the claim earlier. It shipped beside head (return self[:n]) as raw Python slice syntax with a docstring, not indexing machinery.
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 @final on the shared NDFrame base. One real semantic change since: pandas 2.1 (August 2023, issue #54011) made head and tail return deep copies under the new Copy-on-Write regime — previews stopped being writable views.
DataFrame.tail is @final on the shared NDFrame base and literally returns self.iloc[-n:].copy() — the copy is why edits to a peek never leak into the parent, and since 2.1 that copy is deep under CoW (issue #54011). The interesting variant is groupby.tail: instead of iterating groups and calling apply, it builds one positional mask via _make_mask_from_positional_indexer(slice(-n, None)) 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.