
Image by author
Introduction
Parsing dates and times is one of those tasks that looks simple until it is attempted in earnest. Python’s datetime module handles standard formats well, but real-world data is messy: user input, scraped web pages and legacy systems all throw curveballs. This article walks through five practical functions for common date and time parsing tasks. Together they illustrate how to build flexible parsers that cope with the untidy formats that appear in real projects. The complete code is available on GitHub.
1. Parsing relative time strings
Social media apps, chat applications and activity feeds display timestamps such as “5 minutes ago” or “2 days ago.” When that data is scraped or processed, those relative strings need to be converted back into real datetime objects. The following function handles common relative expressions:
from datetime import datetime, timedelta
import re
def parse_relative_time(time_string, reference_time=None):
"""
Convert relative time strings to datetime objects.
Examples: "2 hours ago", "3 days ago", "1 week ago"
"""
if reference_time is None:
reference_time = datetime.now()
# Normalize the string
time_string = time_string.lower().strip()
# Pattern: number + time unit + "ago"
pattern = r'(d+)s*(second|minute|hour|day|week|month|year)s?s*ago'
match = re.match(pattern, time_string)
if not match:
raise ValueError(f"Cannot parse: {time_string}")
amount = int(match.group(1))
unit = match.group(2)
# Map units to timedelta kwargs
unit_mapping = {
'second': 'seconds',
'minute': 'minutes',
'hour': 'hours',
'day': 'days',
'week': 'weeks',
}
if unit in unit_mapping:
delta_kwargs = {unit_mapping(unit): amount}
return reference_time - timedelta(**delta_kwargs)
elif unit == 'month':
# Approximate: 30 days per month
return reference_time - timedelta(days=amount * 30)
elif unit == 'year':
# Approximate: 365 days per year
return reference_time - timedelta(days=amount * 365)The function uses a regular expression (regex) to extract the number and the time unit from the string. The (\d+) group captures one or more digits, and a second group matches the time unit; a trailing s? makes the plural optional, so both “hour” and “hours” work. For units that timedelta supports directly (seconds through weeks), it builds a timedelta and subtracts it from the reference time. Months and years are approximated as 30 and 365 days respectively — imperfect, but adequate for most uses. The reference_time parameter allows a different “now” to be supplied, which is useful for testing or for processing historical data.
Testing it:
result1 = parse_relative_time("2 hours ago")
result2 = parse_relative_time("3 days ago")
result3 = parse_relative_time("1 week ago")
print(f"2 hours ago: {result1}")
print(f"3 days ago: {result2}")
print(f"1 week ago: {result3}")Output:
2 hours ago: 2026-01-06 12:09:34.584107
3 days ago: 2026-01-03 14:09:34.584504
1 week ago: 2025-12-30 14:09:34.5845582. Extracting dates from natural-language text
Sometimes a date is buried in a sentence — “The meeting is scheduled for January 15, 2026” or “Please respond by March 3.” Rather than parse the whole sentence manually, the goal is simply to extract the date. This function finds and extracts dates from natural-language text:
import re
from datetime import datetime
def extract_date_from_text(text, current_year=None):
"""
Extract dates from natural language text.
Handles formats like:
- "January 15th, 2024"
- "March 3rd"
- "Dec 25th, 2023"
"""
if current_year is None:
current_year = datetime.now().year
# Month names (full and abbreviated)
months = {
'january': 1, 'jan': 1,
'february': 2, 'feb': 2,
'march': 3, 'mar': 3,
'april': 4, 'apr': 4,
'may': 5,
'june': 6, 'jun': 6,
'july': 7, 'jul': 7,
'august': 8, 'aug': 8,
'september': 9, 'sep': 9, 'sept': 9,
'october': 10, 'oct': 10,
'november': 11, 'nov': 11,
'december': 12, 'dec': 12
}
# Pattern: Month Day(st/nd/rd/th), Year (year optional)
pattern = r'(january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|sept|october|oct|november|nov|december|dec)s+(d{1,2})(?:st|nd|rd|th)?(?:,?s+(d{4}))?'
matches = re.findall(pattern, text.lower())
if not matches:
return None
# Take the first match
month_str, day_str, year_str = matches(0)
month = months(month_str)
day = int(day_str)
year = int(year_str) if year_str else current_year
return datetime(year, month, day)The function builds a dictionary mapping month names (both full and abbreviated) to their numeric values. The regex matches a month name followed by a day number, with an optional ordinal suffix (st, nd, rd, th) and an optional year. The (?:...) syntax creates a non-capturing group, which matches a pattern without storing it separately — handy for optional parts such as the ordinal suffix and year. When no year is given, the function defaults to the current year, on the reasonable assumption that “March 3” mentioned in January refers to the coming March.
Testing it:
text1 = "The meeting is scheduled for January 15th, 2026 at 3pm"
text2 = "Please respond by March 3rd"
text3 = "Deadline: Dec 25th, 2026"
date1 = extract_date_from_text(text1)
date2 = extract_date_from_text(text2)
date3 = extract_date_from_text(text3)
print(f"From '{text1}': {date1}")
print(f"From '{text2}': {date2}")
print(f"From '{text3}': {date3}")Output:
From 'The meeting is scheduled for January 15th, 2026 at 3pm': 2026-01-15 00:00:00
From 'Please respond by March 3rd': 2026-03-03 00:00:00
From 'Deadline: Dec 25th, 2026': 2026-12-25 00:00:003. Parsing flexible formats with smart detection
Real-world data arrives in many formats, and writing a separate parser for each is impractical. A better approach is a single function that tries multiple formats automatically:
from datetime import datetime
def parse_flexible_date(date_string):
"""
Parse dates in multiple common formats.
Tries various formats and returns the first match.
"""
date_string = date_string.strip()
# List of common date formats
formats = (
'%Y-%m-%d',
'%Y/%m/%d',
'%d-%m-%Y',
'%d/%m/%Y',
'%m/%d/%Y',
'%d.%m.%Y',
'%Y%m%d',
'%B %d, %Y',
'%b %d, %Y',
'%d %B %Y',
'%d %b %Y',
)
# Try each format
for fmt in formats:
try:
return datetime.strptime(date_string, fmt)
except ValueError:
continue
# If nothing worked, raise an error
raise ValueError(f"Unable to parse date: {date_string}")This function uses a brute-force approach, trying each format until one succeeds. The strptime function raises a ValueError when a string does not match the given format; the function catches that exception and moves on to the next candidate. Format order matters. ISO format (%Y-%m-%d) is tried first because it is most common in technical contexts, while ambiguous formats such as %d/%m/%Y and %m/%d/%Y come later. If a particular format dominates the data, reorder the list to prioritize it.
Testing it with several formats:
# Test different formats
dates = (
"2026-01-15",
"15/01/2026",
"01/15/2026",
"15.01.2026",
"20260115",
"January 15, 2026",
"15 Jan 2026"
)
for date_str in dates:
parsed = parse_flexible_date(date_str)
print(f"{date_str:20} -> {parsed}")Output:
2026-01-15 -> 2026-01-15 00:00:00
15/01/2026 -> 2026-01-15 00:00:00
01/15/2026 -> 2026-01-15 00:00:00
15.01.2026 -> 2026-01-15 00:00:00
20260115 -> 2026-01-15 00:00:00
January 15, 2026 -> 2026-01-15 00:00:00
15 Jan 2026 -> 2026-01-15 00:00:00This approach is not the most efficient, but it is simple and handles most of the date formats encountered in practice.
4. Parsing durations
Video players, workout trackers and time-tracking apps display durations like “1 hour 30 minutes” or “2:45:30.” Parsing user input or scraped data of this kind means converting those strings into timedelta objects for calculation. This function handles common duration formats:
from datetime import timedelta
import re
def parse_duration(duration_string):
"""
Parse duration strings into timedelta objects.
Handles formats like:
- "1h 30m 45s"
- "2:45:30" (H:M:S)
- "90 minutes"
- "1.5 hours"
"""
duration_string = duration_string.strip().lower()
# Try colon format first (H:M:S or M:S)
if ':' in duration_string:
parts = duration_string.split(':')
if len(parts) == 2:
# M:S format
minutes, seconds = map(int, parts)
return timedelta(minutes=minutes, seconds=seconds)
elif len(parts) == 3:
# H:M:S format
hours, minutes, seconds = map(int, parts)
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
# Try unit-based format (1h 30m 45s)
total_seconds = 0
# Find hours
hours_match = re.search(r'(d+(?:.d+)?)s*h(?:ours?)?', duration_string)
if hours_match:
total_seconds += float(hours_match.group(1)) * 3600
# Find minutes
minutes_match = re.search(r'(d+(?:.d+)?)s*m(?:in(?:ute)?s?)?', duration_string)
if minutes_match:
total_seconds += float(minutes_match.group(1)) * 60
# Find seconds
seconds_match = re.search(r'(d+(?:.d+)?)s*s(?:ec(?:ond)?s?)?', duration_string)
if seconds_match:
total_seconds += float(seconds_match.group(1))
if total_seconds > 0:
return timedelta(seconds=total_seconds)
raise ValueError(f"Unable to parse duration: {duration_string}")The function handles two main formats: colon-separated times and unit-based strings. For the colon format, it splits on the colon and interprets the parts as hours, minutes and seconds (or just minutes and seconds for a two-part value). For the unit-based format, it uses three regex patterns to find hours, minutes and seconds. For example, (\d+(?:\.\d+)?) matches integers or decimals such as “1.5”, and \s*h(?:ours?)? matches “h”, “hour” or “hours” with optional spaces. Each matched value is converted to seconds and added to the total, allowing the function to handle mixed inputs.
Output:
1h 30m 45s -> 1:30:45
2:45:30 -> 2:45:30
90 minutes -> 1:30:00
1.5 hours -> 1:30:00
45s -> 0:00:45
2h 15m -> 2:15:005. Parsing ISO week dates
Some systems use ISO week dates instead of ordinary calendar dates. An ISO week date such as “2026-W03-2” means “week 3 of 2026, day 2 (Tuesday).” The format is common in business contexts where planning happens weekly. This function parses ISO week dates:
from datetime import datetime, timedelta
def parse_iso_week_date(iso_week_string):
"""
Parse ISO week date format: YYYY-Www-D
Example: "2024-W03-2" = Week 3 of 2024, Tuesday
ISO week numbering:
- Week 1 is the week with the first Thursday of the year
- Days are numbered 1 (Monday) through 7 (Sunday)
"""
# Parse the format: YYYY-Www-D
parts = iso_week_string.split('-')
if len(parts) != 3 or not parts(1).startswith('W'):
raise ValueError(f"Invalid ISO week format: {iso_week_string}")
year = int(parts(0))
week = int(parts(1)(1:)) # Remove 'W' prefix
day = int(parts(2))
if not (1 <= week <= 53):
raise ValueError(f"Week must be between 1 and 53: {week}")
if not (1 <= day <= 7):
raise ValueError(f"Day must be between 1 and 7: {day}")
# Find January 4th (always in week 1)
jan_4 = datetime(year, 1, 4)
# Find Monday of week 1
week_1_monday = jan_4 - timedelta(days=jan_4.weekday())
# Calculate the target date
target_date = week_1_monday + timedelta(weeks=week - 1, days=day - 1)
return target_dateISO week dates follow specific rules: week 1 is the week containing the first Thursday of the year, which means it can begin in December of the previous year. The function uses a reliable approach — find January 4 (always in week 1), locate the Monday of that week, then add the appropriate number of weeks and days to reach the target. The expression jan_4.weekday() returns 0 for Monday through 6 for Sunday; subtracting it from January 4 gives the Monday of week 1, after which (week - 1) weeks and (day - 1) days are added to reach the final date.
Testing it:
# Test ISO week dates
iso_dates = (
"2024-W01-1", # Week 1, Monday
"2024-W03-2", # Week 3, Tuesday
"2024-W10-5", # Week 10, Friday
)
for iso_date in iso_dates:
parsed = parse_iso_week_date(iso_date)
print(f"{iso_date} -> {parsed.strftime('%Y-%m-%d (%A)')}")Output:
2024-W01-1 -> 2024-01-01 (Monday)
2024-W03-2 -> 2024-01-16 (Tuesday)
2024-W10-5 -> 2024-03-08 (Friday)This format is less common than ordinary dates, but the parser makes it straightforward to handle when it appears.
Limitations and what to watch
These functions are deliberately lightweight and are best suited to small scripts, prototypes and learning projects, where adding heavy dependencies would be overkill. Several caveats are worth keeping in mind. The month and year approximations in the relative-time parser (30 and 365 days) drift over long spans and ignore leap years, so they should not be used where exact calendar arithmetic matters. The flexible-format parser cannot resolve genuinely ambiguous inputs — “01/02/2026” is January 2 or February 1 depending on locale — so the format order encodes an assumption that should be made explicit. None of these functions handle time zones or daylight-saving transitions, which are a frequent source of subtle bugs in production systems.
For anything mission-critical, established libraries such as python-dateutil and Pendulum handle many of these edge cases — fuzzy parsing, time zones, relative deltas — with far more rigor, and Python 3.9+ ships the standard-library zoneinfo module for time-zone-aware calculations. Rolling a custom parser is valuable for understanding how parsing works and for the rare format the standard tools miss, not as a wholesale replacement for them.
Wrapping up
Each function here combines regex patterns with datetime arithmetic to absorb variation in formatting, and the same techniques transfer to other parsing challenges. Writing a parser by hand builds a working understanding of how date parsing operates — so that when a non-standard format appears that the standard libraries cannot handle, a custom solution is within reach.
Originally written by Bala Priya C, a developer and technical writer whose interests span mathematics, programming, data science, DevOps and natural language processing.