Pandas to_datetime(): Parse, Clean and Validate Dates

pd.to_datetime() converts date text, numeric timestamps or separate date components into datetime values. Define the source format and timezone, then inspect invalid inputs before reporting. A valid parse can still use the wrong day/month order or fall outside your reporting period.

Convert a Date Column with an Explicit Format

Use the top-level pd.to_datetime() function and assign its result to the column. There is no DataFrame.to_datetime() method. These student dates use month/day/year, so 2/1/2020 means February 1. The dtype is datetime rather than text; its resolution can vary across Pandas versions.

import pandas as pd

def student_dates():
    return pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"],
                         "dt_start": ["1/1/2020", "2/1/2020", "5/1/2020"]})

my_data = student_dates()
my_data["dt_start"] = pd.to_datetime(my_data["dt_start"], format="%m/%d/%Y")
print(my_data)
print(my_data.dtypes)
assert my_data.loc[1, "dt_start"] == pd.Timestamp("2020-02-01")

Expected output

   NAME   dt_start
0  Ravi 2020-01-01
1  Raju 2020-02-01
2  Alex 2020-05-01
NAME                   str
dt_start    datetime64[us]
dtype: object

Parse Dates with Hyphens

Match the source separator in the format. Keeping a known format explicit avoids guessing whether 2-1 means February 1 or January 2. The resulting date values can be sorted and filtered as dates.

my_data = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"],
                        "dt_start": ["1-1-2020", "2-1-2020", "5-1-2020"]})
my_data["dt_start"] = pd.to_datetime(my_data["dt_start"], format="%m-%d-%Y")
print(my_data)

Expected output

   NAME   dt_start
0  Ravi 2020-01-01
1  Raju 2020-02-01
2  Alex 2020-05-01

Extract Year, Month and Day

Use the Series .dt accessor for a datetime column. The dates with abbreviated month names are parsed with an explicit format. See Pandas date and time for other date operations.

my_data = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"],
                        "dt_start": ["1-Jan-2020", "2-Feb-2020", "5-Mar-2020"]})
my_data["dt_start"] = pd.to_datetime(my_data["dt_start"], format="%d-%b-%Y")
my_data["year"] = my_data["dt_start"].dt.year
my_data["month"] = my_data["dt_start"].dt.month
my_data["day"] = my_data["dt_start"].dt.day
print(my_data)

Expected output

   NAME   dt_start  year  month  day
0  Ravi 2020-01-01  2020      1    1
1  Raju 2020-02-02  2020      2    2
2  Alex 2020-03-05  2020      3    5

dayfirst: Resolve the Intended Date Order

With dayfirst=True, Pandas prefers day/month order, but this preference is not strict validation. If the file contract is day/month/year, use format="%d/%m/%Y". A valid but misinterpreted date does not become NaT: both interpretations of 02/01/2020 are valid.

my_data = student_dates()
my_data["dt_start"] = pd.to_datetime(my_data["dt_start"], dayfirst=True)
print(my_data)
strict = pd.to_datetime(student_dates()["dt_start"], format="%d/%m/%Y")
assert my_data["dt_start"].equals(strict)
assert strict.iloc[1] == pd.Timestamp("2020-01-02")

Expected output

   NAME   dt_start
0  Ravi 2020-01-01
1  Raju 2020-01-02
2  Alex 2020-01-05

yearfirst and Year-First Text

yearfirst=True is also a preference rather than a strict format rule. For these year/month-name/day values, the format expresses the source layout directly. Use a four-digit year when preparing new datasets.

my_data = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"],
                        "dt_start": ["2020-Jan-2", "2021-Feb-5", "2019-Mar-12"]})
preferred = pd.to_datetime(my_data["dt_start"], yearfirst=True)
my_data["dt_start"] = pd.to_datetime(my_data["dt_start"], format="%Y-%b-%d")
assert preferred.equals(my_data["dt_start"])
print(my_data)

Expected output

   NAME   dt_start
0  Ravi 2020-01-02
1  Raju 2021-02-05
2  Alex 2019-03-12

Parse Compact YYYYMMDD Dates

A compact date needs a format. Treat identifiers such as 22000105 as date text, rather than accidentally interpreting them as elapsed nanoseconds. Date ranges depend on the chosen datetime resolution.

raw = pd.Series(["22000105", "20200102"])
dates = pd.to_datetime(raw, format="%Y%m%d")
print(dates)
assert dates.iloc[0].year == 2200

Expected output

0   2200-01-05
1   2020-01-02
dtype: datetime64[us]

errors: Raise or Convert Invalid Values to NaT

The default errors="raise" stops on an invalid date. errors="coerce" gives NaT, the missing datetime value. Current Pandas supports raise and coerce; errors="ignore" is no longer supported. Preserve raw values so that coercion does not hide the reason a record needs review.

raw = pd.Series(["2024-02-29", "2024-02-30", "not a date", None])
try:
    pd.to_datetime(raw, format="%Y-%m-%d", errors="raise")
except ValueError:
    print("Invalid date detected; inspect source values.")
parsed = pd.to_datetime(raw, format="%Y-%m-%d", errors="coerce")
print(pd.DataFrame({"raw": raw, "parsed": parsed}))
assert parsed.isna().sum() == 3

Expected output

Invalid date detected; inspect source values.
          raw     parsed
0  2024-02-29 2024-02-29
1  2024-02-30        NaT
2  not a date        NaT
3         NaN        NaT

Out-of-Range Dates Depend on Resolution

A year such as 1100 is not universally invalid: a second or microsecond resolution can represent dates outside the nanosecond range. To demonstrate a nanosecond limitation explicitly, convert the parsed value with as_unit(). For validation, define your business date range separately.

historical = pd.to_datetime("11000101", format="%Y%m%d")
print("Historical date:", historical.strftime("%Y-%m-%d"))
try:
    historical.as_unit("ns")
except pd.errors.OutOfBoundsDatetime:
    print("This date cannot be represented at nanosecond resolution.")

Expected output

Historical date: 1100-01-01
This date cannot be represented at nanosecond resolution.

Present Date and Time

A scalar gives a Timestamp, whose fields are accessed directly. Use pd.Timestamp.now(tz="UTC") for an explicit timezone. This cell prints a live value; its output will differ each run. Formatting changes the displayed string, not the underlying instant.

present_time = pd.Timestamp.now(tz="UTC")
print("Current UTC:", present_time.isoformat())
print("Display:", present_time.strftime("%d-%b-%Y %H:%M:%S %Z"))
print("Hour:", present_time.hour)
print("Minute:", present_time.minute)
print("Second:", present_time.second)
print("Year:", present_time.year)
print("Month:", present_time.month)
print("Day:", present_time.day)
print("Weekday (Monday=0):", present_time.dayofweek)
print("Day name:", present_time.day_name())

Example output (changes each run)

Current UTC: 2026-10-07T06:14:59.758426+00:00
Display: 07-Oct-2026 06:14:59 UTC
Hour: 6
Minute: 14
Second: 59
Year: 2026
Month: 10
Day: 7
Weekday (Monday=0): 2
Day name: Wednesday

Parse a Date with Hours, Minutes and Seconds

Match each component: %m month, %d day, %Y year, %H 24-hour clock, %M minutes and %S seconds. Use strftime() to create a display string after parsing.

present_time = pd.to_datetime("12252020:233445", format="%m%d%Y:%H%M%S")
print("HOUR:", present_time.hour)
print("Minutes:", present_time.minute)
print("Seconds:", present_time.second)
print("Year:", present_time.year)
print("Month:", present_time.month)
print("Day:", present_time.day)
print("dayofweek:", present_time.dayofweek)
print("day_name:", present_time.day_name())
assert present_time == pd.Timestamp("2020-12-25 23:34:45")

Expected output

HOUR: 23
Minutes: 34
Seconds: 45
Year: 2020
Month: 12
Day: 25
dayofweek: 4
day_name: Friday

Mixed Formats: Use a Known Source Policy

Prefer one documented format. format="mixed" infers each item separately and can silently interpret ambiguous dates differently from the source intent. It is appropriate here because the synthetic source declares day-first values. It does not establish that a real ambiguous value is correct.

raw = pd.Series(["2024-01-31", "01/02/2024", "3 Mar 2024"])
parsed = pd.to_datetime(raw, format="mixed", dayfirst=True, errors="raise")
print(parsed)
assert parsed.iloc[1] == pd.Timestamp("2024-02-01")

Expected output

0   2024-01-31
1   2024-02-01
2   2024-03-03
dtype: datetime64[us]

ISO Timestamps and Mixed Offsets

Use format="ISO8601" for differing ISO layouts. utc=True converts timezone-aware inputs to UTC and avoids mixed-offset problems. If an input has no timezone, utc=True assumes that value already describes UTC; it does not discover the local timezone.

raw = pd.Series(["2024-01-01T10:00:00+05:30", "2024-01-01T04:30:00Z"])
parsed = pd.to_datetime(raw, format="ISO8601", utc=True)
print(parsed)
assert parsed.iloc[0] == parsed.iloc[1]

Expected output

0   2024-01-01 04:30:00+00:00
1   2024-01-01 04:30:00+00:00
dtype: datetime64[us, UTC]

Local Wall-Clock Times: Localize before Converting

For a source declared to be India local time, parse the naive clock value, attach Asia/Kolkata, then convert to UTC. Other timezones can have ambiguous or nonexistent daylight-saving times that need an explicit policy. Converting a timezone and attaching a timezone are different operations.

raw = pd.Series(["2024-01-01 10:00:00"])
local_clock = pd.to_datetime(raw, format="%Y-%m-%d %H:%M:%S")
local_time = local_clock.dt.tz_localize("Asia/Kolkata")
print("Source time:")
print(local_time)
print("UTC:")
print(local_time.dt.tz_convert("UTC"))
assert local_time.dt.tz_convert("UTC").iloc[0].hour == 4

Expected output

Source time:
0   2024-01-01 10:00:00+05:30
dtype: datetime64[us, Asia/Kolkata]
UTC:
0   2024-01-01 04:30:00+00:00
dtype: datetime64[us, UTC]

Numeric Timestamps: Specify unit and origin

Seconds and milliseconds since the Unix epoch use different units. Without unit, integers can be interpreted as nanoseconds. Use exact integer values when possible; floating-point timestamps can lose precision. A custom origin is useful for a documented elapsed-day counter.

seconds = pd.to_datetime([1704067200], unit="s", origin="unix", utc=True)
milliseconds = pd.to_datetime([1704067200000], unit="ms", origin="unix", utc=True)
print(seconds)
assert seconds.equals(milliseconds)
print(pd.to_datetime([1, 2, 3], unit="D", origin="1960-01-01"))

Expected output

DatetimeIndex(['2024-01-01 00:00:00+00:00'], dtype='datetime64[s, UTC]', freq=None)
DatetimeIndex(['1960-01-02', '1960-01-03', '1960-01-04'], dtype='datetime64[s]', freq=None)

Assemble Dates from Separate Columns

Pass year, month and day columns to pd.to_datetime. Column names define the components; format has no effect on DataFrame input. Invalid combinations can be coerced and reviewed.

parts = pd.DataFrame({"year": [2024, 2024], "month": [2, 2], "day": [29, 30]})
parsed = pd.to_datetime(parts, errors="coerce")
print(parsed)
assert parsed.notna().sum() == 1

Expected output

0   2024-02-29
1          NaT
dtype: datetime64[us]

Practical Cleaning: Keep Raw Dates and Review Flags

This synthetic sales import uses day/month/year. Strip surrounding whitespace, distinguish missing input from invalid input, and retain the raw column. A separate business rule accepts only January 2024 dates. See data cleaning for a broader workflow; replacing bad dates with today would invent information.

def orders_data():
    return pd.DataFrame({"order_id": [101, 102, 103, 104, 105, 106],
                         "date_raw": [" 01/01/2024 ", "15/01/2024", "31/02/2024", "", None, "01/01/2099"],
                         "revenue": [120, 80, 50, 30, 20, 100]})

orders = orders_data()
text = orders["date_raw"].astype("string").str.strip()
missing = text.isna() | text.eq("").fillna(False)
orders["order_date"] = pd.to_datetime(text, format="%d/%m/%Y", errors="coerce")
orders["missing_input"] = missing
orders["invalid_input"] = ~missing & orders["order_date"].isna()
orders["outside_period"] = orders["order_date"].notna() & ~orders["order_date"].between("2024-01-01", "2024-01-31")
orders["needs_review"] = orders[["missing_input", "invalid_input", "outside_period"]].any(axis=1)
print(orders.to_string(index=False))
assert orders["missing_input"].sum() == 2
assert orders["invalid_input"].sum() == 1
assert orders["outside_period"].sum() == 1
assert orders["needs_review"].sum() == 4

Expected output

 order_id     date_raw  revenue order_date  missing_input  invalid_input  outside_period  needs_review
      101  01/01/2024       120 2024-01-01          False          False           False         False
      102   15/01/2024       80 2024-01-15          False          False           False         False
      103   31/02/2024       50        NaT          False           True           False          True
      104                    30        NaT           True          False           False          True
      105          NaN       20        NaT           True          False           False          True
      106   01/01/2099      100 2099-01-01          False          False            True          True

Report Accepted Dates without Losing Rejected Records

Only accepted records contribute to this January revenue report. Keep a review table and reconcile record counts and revenue to the original import. A valid parse alone is insufficient: the year 2099 parses successfully but violates the reporting period. Use resample() for date-based summaries.

accepted = orders.loc[~orders["needs_review"]].copy()
review = orders.loc[orders["needs_review"]].copy()
monthly = accepted.set_index("order_date")["revenue"].resample("MS").sum()
print("January accepted revenue:")
print(monthly)
print("Review IDs:", review["order_id"].tolist())
assert len(accepted) + len(review) == len(orders)
assert accepted["revenue"].sum() == 200
assert accepted["revenue"].sum() + review["revenue"].sum() == orders["revenue"].sum() == 400

Expected output

January accepted revenue:
order_date
2024-01-01    200
Freq: MS, Name: revenue, dtype: int64
Review IDs: [103, 104, 105, 106]

Common Date Conversion Problems

Month and day swapped: inspect the source date convention and specify format. NaT appeared: compare raw values with isna() and separate blank inputs from invalid dates. .dt fails: ensure the result was assigned to the column and has a datetime dtype. Unexpected 1970 date: inspect numeric unit and origin. Timezone error: define whether the source is UTC, offset-aware or local wall-clock time. Range changed after conversion: check datetime resolution. Parsing formats and display formats solve different problems; strftime returns text.

Exercise: Review a Day-First Import

Parse 05/01/2024, 31/04/2024 and a blank value as day/month/year. Keep raw values and distinguish an invalid nonblank date from missing input. Predict: one valid date, one invalid date and one missing value. Continue with date and time Exercise 3 and Exercise 3-2.

Exercise Solution

Do not count every NaT as an invalid supplied date: an empty field is a missing input. Validate both flags independently.

raw = pd.Series(["05/01/2024", "31/04/2024", ""], dtype="string")
missing = raw.isna() | raw.str.strip().eq("").fillna(False)
parsed = pd.to_datetime(raw, format="%d/%m/%Y", errors="coerce")
invalid = ~missing & parsed.isna()
print(pd.DataFrame({"raw": raw, "parsed": parsed, "missing": missing, "invalid": invalid}))
assert parsed.notna().sum() == 1
assert missing.sum() == invalid.sum() == 1
assert parsed.iloc[0] == pd.Timestamp("2024-01-05")

Expected output

          raw     parsed  missing  invalid
0  05/01/2024 2024-01-05    False    False
1  31/04/2024        NaT    False     True
2                    NaT     True    False

Practice in Google Colab

Open in Google Colab View on GitHub
All synthetic data is included. Run the examples, inspect review flags and try the exercise. Save a copy in Drive before editing.

Continue with Pandas date and time, date_range(), period_range(), strftime() and to_timedelta(). Reference: Pandas to_datetime documentation.




Subscribe to our YouTube Channel here



plus2net.com







Python Video Tutorials
Python SQLite Video Tutorials
Python MySQL Video Tutorials
Python Tkinter Video Tutorials
✖
We use cookies to improve your browsing experience. . Learn more
HTML MySQL PHP JavaScript ASP Photoshop Articles Contact us
© 2000-2026 plus2net.com All rights reserved worldwide Privacy Policy Disclaimer