Formatting and Parsing

Date and Time in Practice

Chapter 2 ยท Formatting and Parsing

Date and Time in Practice 1 introduced strftime and strptime. This chapter goes deeper: the format codes you will actually use, the ways date text can be ambiguous, the stricter and safer fromisoformat, and a pattern for handling input that does not parse. The examples use Python 3.14, and every snippet was run; the output shown in the comments is what Python printed.

The Format Codes Worth Knowing

All the examples below format the same moment, datetime(2026, 3, 5, 9, 7, 4, 123456), which is a Thursday.

CodeMeaningOutput
%y2-digit year26
%a %bshort weekday; short monthThu Mar
%I %phour 01-12; AM or PM09 AM
%fmicroseconds, 6 digits123456
%jday of the year, 001-366064
%w %uweekday number: Sunday is 0; Monday is 14 4
%V %GISO week number; ISO year of that week10 2026
%%a literal percent sign%
from datetime import date, datetime dt = datetime(2026, 3, 5, 9, 7, 4, 123456) print(dt.strftime("%d/%m/%y")) # 05/03/26 print(dt.strftime("%a %b %d")) # Thu Mar 05 print(dt.strftime("%I:%M %p")) # 09:07 AM print(dt.strftime("%H:%M:%S.%f")) # 09:07:04.123456 print(dt.strftime("%G-W%V-%u")) # 2026-W10-4 print(dt.strftime("%j")) # 064 # An f-string accepts the same codes after a colon print(f"{dt:%d %B %Y}") # 05 March 2026 print(dt.isoformat(timespec="minutes")) # 2026-03-05T09:07 print(dt.isoformat(sep=" ", timespec="seconds")) # 2026-03-05 09:07:04
Names depend on the locale
%A, %B, %a, %b and %p produce names from the machine's current locale. The output above is English because the program ran in an English locale; on a machine set to another language the same code gives different words, and text in that language will not parse on an English machine. Names are fine for display, but do not use them in stored or exchanged data.

Parsing with strptime

The format must describe the text. Names, 12-hour clocks and two-digit years all parse, but each has a catch worth knowing.

print(datetime.strptime("5 March 2026", "%d %B %Y")) # 2026-03-05 00:00:00 print(datetime.strptime("5 Mar 26", "%d %b %y")) # 2026-03-05 00:00:00 print(datetime.strptime("3:07 PM", "%I:%M %p")) # 1900-01-01 15:07:00

Three things are visible there. Fields missing from the format are filled with defaults, so a time with no date becomes 1 January 1900, a placeholder you should ignore (take .time() from it). Text with no time of day gives midnight. And a two-digit year has to be guessed:

print(datetime.strptime("69", "%y").year) # 1969 print(datetime.strptime("68", "%y").year) # 2068

Python maps 69 to 99 onto the 1900s and 00 to 68 onto the 2000s. A birth year written 68 becomes 2068, which is in the future. Prefer four-digit years whenever you have a choice.

Ambiguous Text

Parsing succeeds whenever the text fits the format, even if the format is the wrong one. The same text can be valid under two formats and mean two different days:

text = "1/2/26" print(datetime.strptime(text, "%d/%m/%y").date()) # 2026-02-01 (day first: 1 February) print(datetime.strptime(text, "%m/%d/%y").date()) # 2026-01-02 (month first: 2 January)

Nothing in the code can tell you which was meant. Only the context can: where the data came from, or whether another date in the same file has a first number above 12. When you must accept day-first or month-first text, make the choice explicit and documented, or ask the person. When you can choose, switch to ISO YYYY-MM-DD, where the order is fixed.

TextDay firstMonth firstVerdict
03/04/20263 April4 Marchambiguous
13/04/202613 Aprilinvalidonly one reading works
2026-04-03always year, month, dayunambiguous

Strict and Lenient Parsing

strptime is stricter than you might expect about extra text, and more relaxed than you might expect about padding:

# Leading zeros are optional when parsing print(datetime.strptime("2026-3-5", "%Y-%m-%d")) # 2026-03-05 00:00:00 # Anything left over is an error try: datetime.strptime("2026-03-05 extra", "%Y-%m-%d") except ValueError as e: print("ValueError:", e) # ValueError: unconverted data remains: extra # So is a date that does not exist try: datetime.strptime("2026-02-29", "%Y-%m-%d") except ValueError as e: print("ValueError:", e) # ValueError: day 29 must be in range 1..28 for month 2 in year 2026 # Stray spaces around the text are also an error, so strip first print(datetime.strptime(" 2026-03-05".strip(), "%Y-%m-%d")) # 2026-03-05 00:00:00

fromisoformat: the Safe Default

When the text is ISO 8601, use fromisoformat. It needs no format string, so there is nothing to get wrong. From Python 3.11 it accepts most ISO forms, not only the one isoformat() writes. Note the difference between date and datetime:

for s in ["2026-03-05", "2026-03-05T09:07:04", "2026-03-05 09:07:04", "20260305", "2026-W10-4"]: for fn in (date.fromisoformat, datetime.fromisoformat): try: print(fn.__qualname__, s, "->", fn(s)) except ValueError: print(fn.__qualname__, s, "-> ValueError") # date.fromisoformat 2026-03-05 -> 2026-03-05 # datetime.fromisoformat 2026-03-05 -> 2026-03-05 00:00:00 # date.fromisoformat 2026-03-05T09:07:04 -> ValueError # datetime.fromisoformat 2026-03-05T09:07:04 -> 2026-03-05 09:07:04 # date.fromisoformat 2026-03-05 09:07:04 -> ValueError # datetime.fromisoformat 2026-03-05 09:07:04 -> 2026-03-05 09:07:04 # date.fromisoformat 20260305 -> 2026-03-05 # datetime.fromisoformat 20260305 -> 2026-03-05 00:00:00 # date.fromisoformat 2026-W10-4 -> 2026-03-05 # datetime.fromisoformat 2026-W10-4 -> 2026-03-05 00:00:00

A date-only function rejects text that carries a time, so use datetime.fromisoformat(s).date() if the input might have either. The ISO week form 2026-W10-4 (year, week 10, day 4) is understood too. Versions of Python before 3.11 were stricter, so check your version if a form that works here fails elsewhere.

Handling Bad Input

Anything a person typed, or another system sent, can fail to parse. The pattern is to try, catch ValueError, and decide what failure means for your program: reject, ask again, or try another format. Do not use a bare except, which would hide real bugs.

def parse_date(text, formats): """Return the first format that fits as a date, or None.""" for f in formats: try: return datetime.strptime(text.strip(), f).date() except ValueError: pass return None formats = ["%Y-%m-%d", "%d %b %Y", "%B %d, %Y"] for t in ["2026-11-02", "2 Nov 2026", "November 2, 2026", "2/11/2026", "2026-02-30"]: print(repr(t), parse_date(t, formats)) # '2026-11-02' 2026-11-02 # '2 Nov 2026' 2026-11-02 # 'November 2, 2026' 2026-11-02 # '2/11/2026' None # '2026-02-30' None

The list of formats is a deliberate choice: 2/11/2026 is rejected because no listed format is day-first with slashes. Accepting fewer formats is safer than guessing. Order matters when two formats could fit the same text, since the first match wins.

A habit worth forming
Display in whatever style suits the reader, but store and exchange ISO text. Parse with fromisoformat when the input is ISO, and with an explicit, short list of formats otherwise. Treat a parse failure as normal data, not as a surprise.

Challenges

Exercise 1

Write readings(text) that tries the text as both day-first (%d/%m/%Y) and month-first (%m/%d/%Y) and returns the sorted list of distinct dates that are valid. Then print, for each of 03/04/2026, 13/04/2026, 05/05/2026 and 31/02/2026, the dates in ISO form and a verdict of ambiguous, clear or invalid. Decide what you expect for 05/05/2026 before running it.

๐Ÿ“„ View solution
Exercise 2

Write to_iso(text) that converts text such as 05 Mar 2026 09:07 PM into ISO form with minutes precision (2026-03-05T21:07), and returns "invalid: " followed by the text if it does not parse. Test it with that example, with 12:00 AM and 12:00 PM on the same day, with 13:00 PM, and with 31 Apr 2026 01:00 AM. Predict what the two twelve o'clock cases give.

๐Ÿ“„ View solution
Exercise 3

Using datetime(2026, 3, 5, 9, 7, 4), build a single line of text in the form Thu 05 Mar 2026, week 10, day 064 with one strftime call. Then check your answer by parsing the date part back with strptime and confirming it equals the original date. Hint: one code gives the ISO week and one gives the day of the year; a comma is an ordinary character.

๐Ÿ“„ View solution

Quick Reference

  • Common codes: %y short year, %a %b short names, %I %p 12-hour clock, %f microseconds, %j day of year, %V ISO week
  • Names and AM/PM follow the locale: display only, never store
  • Missing fields default (time of day to midnight, a bare time to 1 January 1900)
  • Two-digit years: 69-99 become 19xx, 00-68 become 20xx; prefer four digits
  • Ambiguity: 1/2/26 fits both day-first and month-first; only context decides
  • strptime errors: leftover text and impossible dates raise ValueError; strip spaces first
  • ISO input: fromisoformat; use datetime.fromisoformat(s).date() if a time may be present
Coming next
Date and Time in Practice 3 looks at the difference between naive and aware datetimes: what attaching a time zone really does, why mixing the two fails, and how comparing works.