Skip to content

TypeError: Passing PeriodDtype data is invalid. Use data.to_timestamp() instead

Have you ever grouped some data by month, gone to plot the result, and been
stopped by this?

df['month'] = df['when'].dt.to_period('M')
pd.to_datetime(df['month'])
# TypeError: Passing PeriodDtype data is invalid. Use `data.to_timestamp()` instead

The fix is right there in the message, and it is one call:

df['month'].dt.to_timestamp()      # a Series of periods
month_index.to_timestamp()         # a PeriodIndex

Note that the Series needs .dt and the index does not. That small asymmetry
is, in my experience, the reason people read the error, try the fix, and remain
stuck.

A period is not a moment

The error is really about a distinction Pandas takes seriously and most of us
do not think about until something breaks.

A Timestamp is an instant: 2026-01-05 14:30:00. A Period is a span with a
frequency attached — "January 2026", "Q1 2026", "the week of the 5th". When you
call .dt.to_period('M'), you are telling Pandas you no longer care which day
it was, only which month.

to_datetime exists to turn things into instants. A span is not an instant, and
January 2026 has no single moment that stands for it. Rather than guess which
one you meant, Pandas stops and asks you to say so.

to_timestamp() is you saying so. By default it hands back the beginning of the
span:

s = pd.Series(pd.to_datetime(['2026-01-05', '2026-02-11'])).dt.to_period('M')
s.dt.to_timestamp()
# 0   2026-01-01
# 1   2026-02-01

The 5th became the 1st. That is worth pausing on: the original day is not
recovered, because it was discarded back when you created the period, not now.
If you still needed it, you needed a second column.

The three ways in

pd.to_datetime(period_series)      # TypeError
pd.to_datetime(period_index)       # TypeError
pd.DatetimeIndex(period_index)     # TypeError

All three end up in the same place inside Pandas — maybe_convert_dtype in
core/arrays/datetimes.py — which is why the wording is identical no matter
which route you took.

If you came here from a pivot table

Most people searching for this error mention pivot_table, so it is worth
saying plainly: on current Pandas, pivot_table does not raise it. A period
column works perfectly well as an index, as columns, or in a groupby:

df.pivot_table(index='month', columns='region', values='amount')   # fine
df.groupby('month')['amount'].sum()                                # fine
df.set_index('month')                                              # fine

What usually happened is a two-step story. You created a period column because
you wanted to aggregate by month, and that part worked. Later — plotting the
result, merging it against a datetime column, writing it to a database — you
tried to turn the period back into a date, and that is the line that failed.

So when you read the traceback, look for the to_datetime or DatetimeIndex
call. It is often several lines below the code you were editing, which is why
the pivot gets the blame.

Often you do not need to convert at all

Before reaching for to_timestamp, it is worth asking whether you need a
timestamp. Period is a good dtype. It sorts correctly, it groups correctly, and
it prints as 2026-01 rather than 2026-01-01 00:00:00, which is usually what
you wanted in a report anyway.

The cases where you genuinely do need to convert:

For grouping, sorting, resampling and display, leave it alone.

The other direction always works

Going from a moment to a span never raises, because any given instant falls
inside exactly one month:

df['when'].dt.to_period('M')       # datetime -> period, always fine

That asymmetry is the thing to remember. Narrowing an instant down to a span is
automatic. Widening a span back out to an instant requires a decision, and
to_timestamp() is where you make it.

What about astype(str)?

It does work:

pd.to_datetime(df['month'].astype(str))

But you have gone from period to string to timestamp, paid for two conversions
instead of one, and thrown away the frequency along the way. to_timestamp()
does the job in a single step and leaves Pandas knowing what it is holding. Save
astype(str) for when you actually want text.

Practice

Period columns turn up whenever you summarize a time series by month or quarter,
which is most of the time series worth summarizing. See
BW #86: FEMA disaster declarations for one on real
data, and pivot_table in Pandas for the aggregation that
tends to produce the period column in the first place.