LESSON 167 · PRACTICAL PYTHON27 min

Pandas to_datetime: Parse Dates and Find Invalid Rows

Convert date strings to UTC timestamps while identifying invalid or missing source values.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Pandas to_datetime: Parse Dates and Find Invalid Rows

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Here are three values from an order export. The first is February twenty-ninth in a leap year, the second says February thirtieth, and the third is missing. All three are strings or empty values until we parse them. A string column can look convincing on a screen and still behave incorrectly in time calculations. Today we will use pandas to_datetime to create a real datetime Series, identify every invalid source position, and make our timezone assumption explicit. The source contract says the values use four-digit year, two-digit month, and two- digit day. It also says a valid date represents midnight UTC.

  2. 00:41

    Those details are decisions from the data producer, not guesses pandas can safely make for every application. Watch how the first attempt reacts to an impossible date. Imagine a CSV with ten thousand orders: a single visually plausible bad date can distort a monthly total if it is left as text. The parser should therefore expose uncertainty, rather than quietly producing a column that merely looks date-shaped. The strict call uses the stated format but leaves the default error behavior in place. It raises on February thirtieth. That is useful if one bad value should abort the import immediately; the error

  3. 01:21

    prevents the program from pretending that the data is clean. But this particular import workflow needs to report all invalid rows at once. A try block around the call can catch the exception, yet it does not hand us a mask for every source position. Notice that the input's order matters. If we drop or reorder rows too early, we might show a user the wrong record when explaining a bad date. We will switch to a mode that marks invalid values, keep the original strings beside the parsed values, and then inspect the mask. Strict parsing is especially useful during development because it exposes an unexpected source format immediately.

  4. 02:03

    During operations, a complete error list can be more helpful to the person who repairs the export. Choose that behavior explicitly at the import boundary. With errors set to coerce, pandas returns NaT for an impossible date. NaT is the missing datetime marker, not a substitute date and not midnight. The isna mask is true for the invalid calendar day and for the original missing value. We can use that mask to print the raw values that need review. This lets a file importer choose an explicit policy: reject the whole file, quarantine selected records, or ask the data producer to repair them.

  5. 02:44

    The mask alone cannot tell whether a value was blank or malformed; the original column supplies that context. Also notice that an unspecified format invites inference. Since our producer promised year-month- day, the corrected code states that format instead of relying on what looks familiar in this tiny sample. Notice that isna also marks the original None. The parsed mask tells us where the result is unavailable, while the raw values tell us why each row needs investigation. Keep both columns until the import decision has been recorded. The corrected function accepts a sequence, builds a Series to preserve row positions, and calls pandas to_datetime with an explicit format, errors coerce, and UTC

  6. 03:27

    enabled. We return the datetime Series itself. Formatting it as text would discard the benefits of datetime-aware filtering and arithmetic. The sample print formats values only for display, replacing NaT with the word INVALID. UTC needs a careful explanation: a naive date has no timezone. In this example we deliberately interpret midnight as UTC. If a producer meant midnight in Bratislava, we would localize in that region first, then convert. In other words, UTC is a declared part of this data contract, not a universal repair

  7. 04:07

    setting for date strings. Check the dtype or .dt.tz before performing timezone-sensitive operations. An ordinary display of a timestamp may hide its timezone in a compact table. A trustworthy pipeline tests the semantic type, not just a pretty string representation. The final check includes a leap day, an impossible day, a missing value, and a later valid date. We inspect the timezone, the invalid-value mask, and the original order. A date parser should be tested on edge cases because a few normal weekdays do not prove that the calendar contract is enforced.

  8. 04:47

    Keep the raw input for error messages, and resist a silent dropna call before anyone has reviewed the bad records. In a larger pipeline, validate the source column name, record a source row number, and decide whether the entire batch can continue. The key result is a real UTC-aware Series plus a reliable way to locate failures. The related pandas duplicate lesson uses date parsing briefly for ordering; this focused lesson shows what that parsing step must guarantee. One subtle limitation: the format argument describes the intended pattern but does not guarantee zero-padded month and day in every pandas version.

  9. 05:29

    If exact textual width matters, validate the raw string separately before parsing; calendar validity and textual shape are different checks.

Understand the concept

The keyword pandas to_datetime usually appears when strings must become real timestamps for sorting, filtering, or arithmetic. This example expects YYYY-MM-DD. Pass format='%Y-%m-%d' instead of relying on format inference. A source format is part of the data contract and should be checked when the input changes.

The default errors='raise' stops at the first invalid date. A batch import may instead need to report every bad row. With errors='coerce', an impossible date such as 2024-02-30 becomes NaT, pandas' missing datetime value. Use isna() to locate original positions before deciding whether to reject, repair, or quarantine the records. Never silently treat NaT as midnight.

Setting utc=True localizes naive values as UTC. That matches this example's stated contract: a date means midnight UTC. For a different source, such as midnight local time in a named region, first localize in that zone, then convert to UTC. A calendar date may also be better represented as a date rather than an instant; make the domain choice explicit.

The interactive task returns a pandas Series, preserving datetime operations and row order. Its tests include leap day, impossible date, missing value, and UTC zone. The earlier duplicate-removal lesson uses to_datetime briefly to sort contacts; this focused guide explains parsing and invalid-input handling.

  • format states the source contract
  • errors='coerce' produces NaT
  • isna() reveals bad rows
  • utc=True localizes naive values to UTC

See it step by step

Read the code, predict the output, then compare it with the result.

01. Locate invalid positions

PYTHON
import pandas as pd
raw = pd.Series(['2024-02-29', '2024-02-30', None])
parsed = pd.to_datetime(raw, format='%Y-%m-%d', errors='coerce', utc=True)
print(parsed.isna().tolist())
EXPECTED OUTPUT
[False, True, True]

The mask identifies failed values without discarding them.

02. Inspect the timezone

PYTHON
import pandas as pd
parsed = pd.to_datetime(pd.Series(['2024-02-29']), format='%Y-%m-%d', utc=True)
print(str(parsed.dt.tz))
print(parsed.dt.strftime('%Y-%m-%d').tolist())
EXPECTED OUTPUT
UTC
['2024-02-29']

A display string is derived from a UTC-aware datetime Series.

03. Compare strict and coercing modes

PYTHON
import pandas as pd
try:
    pd.to_datetime(['2024-02-30'], format='%Y-%m-%d')
except ValueError:
    print('strict: rejected')
print(pd.isna(pd.to_datetime(['2024-02-30'], format='%Y-%m-%d', errors='coerce')[0]))
EXPECTED OUTPUT
strict: rejected
True

The default raises, while coercion marks a bad value for review.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Declare the source format

The format '%Y-%m-%d' describes year, month, and day separated by hyphens. It communicates the intended input to future maintainers and avoids ambiguous date inference. It is not a fixed-width validator: pandas may still accept a one-digit month or day. If zero padding is mandatory, validate the raw text separately. For a CSV feed, record whether the separator and padding are contractual. Parsing calendar validity and checking exact textual shape are two separate steps, and neither should be assumed from a pretty preview.

Use a different format for timestamps with time or offsets. A date-only source does not itself contain a timezone.

PYTHON
import pandas as pd
s = pd.Series(['2024-02-29', '2024-02-30'])
p = pd.to_datetime(s, format='%Y-%m-%d', errors='coerce')
print(p.isna().tolist())
EXPECTED OUTPUT
[False, True]
Follow the reasoning
  1. Create one leap day and one impossible date.
  2. Parse both with the explicit format.
02 / 03

Report NaT rather than losing rows

NaT is pandas' missing datetime value. It can represent an empty source value or a failed calendar parse. Keep the raw field next to the parsed one when reporting import errors. An error report should include the source row number as well as the raw value. That lets the producer repair the original file without guessing which repeated date string was problematic.

The isna() mask preserves positions. The caller can then decide whether to reject the file or quarantine selected rows.

PYTHON
import pandas as pd
raw = pd.Series(['2024-02-29', '2024-02-30', None], dtype=object)
parsed = pd.to_datetime(raw, format='%Y-%m-%d', errors='coerce')
print(raw[parsed.isna()].tolist())
EXPECTED OUTPUT
['2024-02-30', None]
Follow the reasoning
  1. Find all invalid positions.
  2. Print the original values at those positions.
03 / 03

Choose UTC deliberately

This example defines the date as midnight UTC. utc=True makes that assumption explicit. If the producer means local wall time, localize in its named region first. Timezone conversion can move a timestamp across a calendar-day boundary. If daily reports matter, document whether grouping happens in UTC or in a local reporting zone before comparing dates.

Keep timestamps as timestamps through filtering and arithmetic. Format them only at a display or export boundary.

PYTHON
import pandas as pd
s = pd.to_datetime(pd.Series(['2024-02-29']), format='%Y-%m-%d', utc=True)
print(str(s.dt.tz), s.dt.strftime('%Y-%m-%d').iloc[0])
EXPECTED OUTPUT
UTC 2024-02-29
Follow the reasoning
  1. Parse a date with utc=True.
  2. Check its time zone before formatting.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnParse YYYY-MM-DD values into a UTC-aware pandas Series. Coerce impossible or missing dates to NaT and preserve positions. The sample still prints ['2024-02-29', 'INVALID'].
Need a hint?

Use pd.to_datetime(pd.Series(values), format='%Y-%m-%d', errors='coerce', utc=True). Return the Series itself.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What does errors='coerce' produce for an impossible date?

    02.What does utc=True mean for a naive YYYY-MM-DD value here?

    03.Why keep the result as a Series?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Leaving dates as strings

    COMMON MISTAKE
    sort_values('date_text') on mixed formats
    FIX
    pd.to_datetime(source, format='%Y-%m-%d')

    What happens: Lexical order can differ from chronological order.
    Parse according to a documented source format before date operations.

    Dropping bad rows silently

    COMMON MISTAKE
    pd.to_datetime(values, errors='coerce').dropna()
    FIX
    parsed = pd.to_datetime(values, errors='coerce'); bad = parsed.isna()

    What happens: Invalid source records vanish without an error report.
    Preserve source row identity before filtering.

    Browse this lesson’s problem fixes

    Sources and further reading