Fixing Python datetime.strptime That Raises ValueError on Valid Date Strings
You pull a date string out of a CSV, an API response, or a database row, feed it to datetime.strptime() with what looks like the right format, and Python throws a ValueError. The string is right there on screen. It looks correct. Yet the error keeps happening.
The problem is almost always a tiny mismatch between the string and the format pattern β one you cannot see by skimming both side by side. This guide shows you how to find and fix it systematically.
What you'll learn
- Exactly how
strptimereads a format string character by character - The most common causes of
ValueErrorand how to reproduce each one - How whitespace, locale, and trailing data silently break your format
- Drop-in alternatives when the format is inconsistent or unknown
- A defensive parsing pattern you can reuse in production code
Prerequisites
You need Python 3.7 or later and the standard library only. The optional dateutil section requires python-dateutil, which you can install with pip install python-dateutil. No other dependencies are needed.
How strptime actually matches format to string
datetime.strptime(date_string, format) scans both arguments in lockstep, character by character. Each directive in format (like %Y or %m) consumes a specific number of characters from date_string. Every literal character in the format β spaces, dashes, colons, slashes β must match the exact character at the same position in the input string.
If any character does not match, Python raises:
ValueError: time data '...' does not match format '...'
There is no fuzzy matching, no automatic trimming, and no fallback. The match must be exact from the first character to the last. That strictness is what makes the error so surprising β even a single extra space or a two-digit year where four are expected is enough to fail.
The most common causes of ValueError
Wrong year directive: %y vs %Y
%Y expects a four-digit year (2024). %y expects a two-digit year (24). Swapping them is one of the most frequent mistakes.
from datetime import datetime
# Fails: %Y expects 4 digits, string has 2
datetime.strptime("24-06-15", "%Y-%m-%d") # ValueError
# Correct
datetime.strptime("24-06-15", "%y-%m-%d") # OK
datetime.strptime("2024-06-15", "%Y-%m-%d") # OK
Separator mismatch
The separator in your format must be the exact character used in the string. A slash in the string with a dash in the format will fail immediately.
# Fails: string uses '/', format uses '-'
datetime.strptime("06/15/2024", "%m-%d-%Y") # ValueError
# Correct
datetime.strptime("06/15/2024", "%m/%d/%Y") # OK
12-hour vs 24-hour clock
%H is the 24-hour hour (00β23). %I is the 12-hour hour (01β12). If your string says 14:30 and you use %I, Python will raise a ValueError because 14 is out of range for a 12-hour clock.
# Fails: 14 is not valid for %I
datetime.strptime("14:30", "%I:%M") # ValueError
# Correct
datetime.strptime("14:30", "%H:%M") # OK
# AM/PM strings need %I and %p together
datetime.strptime("02:30 PM", "%I:%M %p") # OK
Zero-padded vs non-padded values
Both %d and %m expect two-digit, zero-padded values. If your source data omits the leading zero (e.g., 6 instead of 06), the match fails. On some platforms you can use %-d or %-m to accept non-padded values, but this is not portable across operating systems.
# May fail on non-padded input
datetime.strptime("6/5/2024", "%m/%d/%Y") # ValueError on many systems
# Portable fix: pad the string yourself before parsing
raw = "6/5/2024"
parts = raw.split("/")
normalized = f"{int(parts[0]):02d}/{int(parts[1]):02d}/{parts[2]}"
datetime.strptime(normalized, "%m/%d/%Y") # OK
Whitespace and invisible characters
Data coming from spreadsheets, APIs, or user input frequently carries invisible baggage. A trailing newline from a CSV row, a non-breaking space from an HTML copy-paste, or an extra space between the date and time are all silent killers.
Always strip the string before parsing:
raw = " 2024-06-15 \n"
clean = raw.strip()
datetime.strptime(clean, "%Y-%m-%d") # OK
If you suspect non-breaking spaces or other Unicode whitespace, use a more aggressive strip:
import re
clean = re.sub(r"\s+", " ", raw).strip()
To inspect exactly what characters are in your string, print their code points:
raw = "2024\u00a006\u00a015" # non-breaking spaces
print([hex(ord(c)) for c in raw])
# ['0x32', '0x30', '0x32', '0x34', '0xa0', '0x36', ...]
# 0xa0 is a non-breaking space, not a regular 0x20 space
This technique is invaluable when you are dealing with date strings scraped from web pages or exported from Excel. If you regularly read dates from CSV files, also check out how Pandas silently misreads date columns as strings β that article covers additional encoding traps at the file-read level.
Locale-dependent directives
The directives %a (abbreviated weekday), %A (full weekday), %b (abbreviated month name), and %B (full month name) are locale-sensitive. If your system locale is not English and your input strings use English month names β or vice versa β strptime will raise a ValueError.
from datetime import datetime
# Works only when LC_TIME is English
datetime.strptime("15 June 2024", "%d %B %Y")
# Fails if locale is set to French or German
The cleanest fix for locale issues is to avoid text-based month names in the format entirely. If you control the data pipeline, switch to numeric formats. When you cannot control the input, set the locale explicitly for the duration of the parse:
import locale
from datetime import datetime
with locale.setlocale(locale.LC_TIME, "en_US.UTF-8"):
result = datetime.strptime("15 June 2024", "%d %B %Y")
Be aware that locale.setlocale is not thread-safe. In a multi-threaded web application, use a dedicated thread or the dateutil library instead.
When the string has extra data
Sometimes the string parses correctly up to a point, then fails because there are extra characters at the end that the format does not account for. Python surfaces this as a different message:
ValueError: unconverted data remains: T14:30:00Z
This happens frequently with ISO 8601 timestamps when you only provide a date-only format:
# Fails: extra time component remains unmatched
datetime.strptime("2024-06-15T14:30:00Z", "%Y-%m-%d") # ValueError
# Correct: match the full string
datetime.strptime("2024-06-15T14:30:00Z", "%Y-%m-%dT%H:%M:%SZ") # OK
# Or, if you only want the date part, slice first
date_only = "2024-06-15T14:30:00Z"[:10]
datetime.strptime(date_only, "%Y-%m-%d") # OK
The slicing approach is pragmatic when the time component varies (e.g., fractional seconds, timezone offsets) and you do not need it. Just make sure the date portion is always exactly 10 characters in your data before relying on a fixed slice.
Safer alternatives when the format is unpredictable
datetime.fromisoformat()
If your strings are ISO 8601 (the format used by most databases and APIs), Python 3.7+ provides datetime.fromisoformat(), which handles a wide range of ISO variants without requiring a format string:
from datetime import datetime
datetime.fromisoformat("2024-06-15") # date only
datetime.fromisoformat("2024-06-15T14:30:00") # date + time
datetime.fromisoformat("2024-06-15 14:30:00") # space separator
In Python 3.11+, fromisoformat() was extended to also accept the Z suffix for UTC. On 3.7β3.10, replace the Z with +00:00 first: s.replace("Z", "+00:00").
dateutil.parser.parse()
When the format is genuinely inconsistent β mixing MM/DD/YYYY and DD-Mon-YYYY in the same column β dateutil can save you significant effort:
from dateutil import parser
parser.parse("June 15, 2024") # datetime(2024, 6, 15, ...)
parser.parse("15/06/2024") # datetime(2024, 6, 15, ...)
parser.parse("2024-06-15") # datetime(2024, 6, 15, ...)
dateutil is more forgiving, but it is also slower and occasionally guesses wrong on ambiguous dates like 01/02/03. Use it as a fallback, not a first resort. When precision matters, an explicit format in strptime is always safer.
A defensive parsing wrapper
In production, you often want to try a known set of formats and fall back gracefully rather than crashing on unexpected input. Here is a reusable helper:
from datetime import datetime
from typing import Optional
KNOWN_FORMATS = [
"%Y-%m-%d",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M:%SZ",
"%d/%m/%Y",
"%m/%d/%Y",
"%d %B %Y",
"%B %d, %Y",
]
def safe_parse(date_string: str) -> Optional[datetime]:
"""Try each known format; return None if all fail."""
clean = date_string.strip()
for fmt in KNOWN_FORMATS:
try:
return datetime.strptime(clean, fmt)
except ValueError:
continue
return None
result = safe_parse(" 15/06/2024 ")
if result is None:
print("Could not parse date")
else:
print(result.isoformat())
This pattern also integrates cleanly with data pipelines. If you are doing similar defensive work around missing rows, the approach to fixing Python sqlite3 queries that return no rows despite matching data uses comparable try/inspect techniques at the query level.
Common pitfalls and gotchas
Microseconds need %f, not %S
%S matches whole seconds only (00β59). If your timestamp includes fractional seconds like 14:30:00.123456, you need %S.%f:
datetime.strptime("2024-06-15 14:30:00.123456", "%Y-%m-%d %H:%M:%S.%f") # OK
datetime.strptime("2024-06-15 14:30:00.123456", "%Y-%m-%d %H:%M:%S") # ValueError
Timezone offsets need %z
A string like 2024-06-15T14:30:00+05:30 requires %z at the end of the format. The colon inside the offset (+05:30) is supported in Python 3.7+, but not in Python 2 or some older 3.x builds on Windows. If %z fails on your platform, strip the offset and attach timezone info manually using pytz or zoneinfo.
%c, %x, and %X are platform-specific
These directives expand to locale-specific representations. They are fine for quick interactive use but will produce different results on different operating systems and locale settings. Avoid them in any code that runs in CI, Docker, or on a server with a non-English locale.
Month numbers vs month names in Pandas
When you parse dates inside a Pandas pipeline rather than in pure Python, the behaviour differs slightly. Pandas has its own to_datetime() with a format argument that mirrors strptime directives, but the error messages look different and silent coercion can mask problems. See the full breakdown of how Pandas misreads date columns for the Pandas-specific version of these issues.
Windows does not support %-d or %-m
The no-padding modifiers %-d and %-m work on Linux and macOS but raise a ValueError on Windows. The portable alternative is to normalise the string yourself (zero-pad or convert to integer) before handing it to strptime.
Next steps
You now have a clear mental model of why strptime fails and a toolkit for fixing it. Here are four concrete things to do next:
- Audit your format strings against the actual data by printing
[hex(ord(c)) for c in sample_string]for any string that raises an unexpectedValueError. - Add
.strip()to everystrptimecall that reads from external sources β it costs nothing and prevents a whole class of bugs. - Switch to
fromisoformat()wherever your data is ISO 8601; it removes the format string entirely and is harder to get wrong. - Use the
safe_parse()wrapper from this article in pipelines where the format can vary, and log everyNonereturn so you know when unexpected formats appear in production. - Consider
dateutil.parser.parse()as a last resort for user-provided or legacy data where format consistency cannot be guaranteed, but always validate the output before storing it.
Frequently Asked Questions
Why does strptime raise ValueError even when my date string looks correct?
The most common cause is a mismatch between a literal character in the format string β such as a separator or a space β and the exact character in the date string. Even a single extra space, a trailing newline, or a slash instead of a dash is enough to trigger the error. Print the character code points of your string with `[hex(ord(c)) for c in s]` to reveal any invisible differences.
How do I parse a date string that has a timezone offset like +05:30 using strptime?
Add `%z` at the end of your format string, for example `"%Y-%m-%dT%H:%M:%S%z"`. Python 3.7+ supports the colon in the offset automatically. If you are on an older environment where `%z` fails, strip the offset from the string and attach timezone info manually using the `zoneinfo` or `pytz` library.
What is the difference between strptime %y and %Y?
`%Y` matches a four-digit year like 2024, while `%y` matches a two-digit year like 24. Using the wrong one is one of the most frequent causes of ValueError in date parsing. Always check which format your source data uses before writing your format string.
How can I parse dates in Python when the format varies between rows?
Use the `safe_parse()` pattern from this article: iterate over a list of known format strings, try each with `strptime` inside a try/except block, and return the first successful result. For highly inconsistent data, `dateutil.parser.parse()` can infer the format automatically, though it may guess incorrectly on ambiguous date strings.
Does datetime.strptime work the same way on Windows and Linux?
Mostly yes, but the no-padding modifiers `%-d` and `%-m` (which suppress leading zeros) work only on Linux and macOS. On Windows they raise a ValueError. The portable fix is to normalise your string to zero-padded values before passing it to strptime.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!