Pattern format

Custom format and parse patterns allow you to format datetime values into strings and parse strings into datetime values, using a pattern string that describes the expected format.

Quick example

>>> from whenever import Date, Time, OffsetDateTime, hours
>>> Date(2024, 3, 15).format("YYYY/MM/DD")
'2024/03/15'
>>> Date.parse("2024/03/15", format="YYYY/MM/DD")
Date("2024-03-15")
>>> OffsetDateTime(2024, 3, 15, 14, 30, offset=+2).format(
...     "ddd, DD MMM YYYY hh:mm:ssxxx"
... )
'Fri, 15 Mar 2024 14:30:00+02:00'

Specifiers

Each pattern is a string containing specifiers and literal text. Specifiers are sequences of the same letter that are replaced by the corresponding value.

Date specifiers

Pattern

Meaning

Example output

Parse support

YYYY

4-digit year, zero-padded

2024

YY

2-digit year

24

❌ format only

MM

Month number (01–12), zero-padded

03

✅ (exactly 2 digits)

M

Month number (1–12), no padding

3

✅ (1–2 digits)

MMM

Abbreviated month name

Mar

✅ (case-insensitive)

MMMM

Full month name

March

✅ (case-insensitive)

DD

Day of month (01–31), zero-padded

05

✅ (exactly 2 digits)

D

Day of month (1–31), no padding

5

✅ (1–2 digits)

ddd

Abbreviated weekday name

Fri

✅ (validated)

dddd

Full weekday name

Friday

✅ (validated)

Note

When parsing, weekday names (ddd/dddd) are validated against the parsed date. A mismatch raises ValueError.

Note

YY is only supported for formatting. When parsing, use YYYY to avoid ambiguity.

Time specifiers

Pattern

Meaning

Example output

Parse support

hh

24-hour (00–23), zero-padded

04, 14

✅ (exactly 2 digits)

h

24-hour (0–23), no padding

4, 14

✅ (1–2 digits)

ii

12-hour (01–12), zero-padded

04, 12

⚠️ (should pair with aa)

i

12-hour (1–12), no padding

4, 12

⚠️ (should pair with aa)

mm

Minute (00–59), zero-padded

05, 30

✅ (exactly 2 digits)

m

Minute (0–59), no padding

5, 30

✅ (1–2 digits)

ss

Second (00–59), zero-padded

05, 45

✅ (exactly 2 digits)

s

Second (0–59), no padding

5, 45

✅ (1–2 digits)

SS

Second, optional (see below)

05, `` (omitted)

ffffffffff

Fractional seconds, exact digits

123 (fff)

FFFFFFFFFF

Fractional seconds, trimmed

12 (FFF)

a

AM/PM first character

P

aa

AM/PM full

PM

Important

  • hh/h are the 24-hour formats. ii/i are the 12-hour formats.

  • Using h/hh with a/aa (AM/PM) raises an error—use i/ii instead.

  • Using i/ii without a/aa emits a warning about ambiguity.

  • The double-letter forms (hh, ii, mm, ss) always zero-pad and require exactly 2 digits when parsing. The single-letter forms (h, i, m, s) skip zero-padding and accept 1–2 digits when parsing.

Optional seconds (SS):

SS omits the seconds component entirely when both seconds and nanoseconds are zero, allowing compact times like 14:30 alongside full times like 14:30:05 in the same format string.

  • When seconds or nanoseconds are non-zero, SS writes two zero-padded digits; :SS additionally writes the preceding colon.

  • When both are zero, nothing is written. The colon disappears with :SS (unlike a bare : literal, which would always be written).

  • During parsing, SS reads two digits if the next character is a digit; :SS reads : followed by two digits if the next character is :. In both cases, if the expected character is absent, seconds is set to zero.

Important

Omission requires both seconds and nanoseconds to be zero. If you have fractional seconds (e.g. 14:30:00.5), SS will write 00 (not omit). If you want compact times and don’t need fractional seconds, call round() first.

>>> Time(14, 30, 0).format("hh:mm:SS")
'14:30'
>>> Time(14, 30, 5).format("hh:mm:SS")
'14:30:05'
>>> Time(14, 30, 0, nanosecond=500_000_000).format("hh:mm:SS")
'14:30:00'
>>> Time(14, 30, 0).format("hh:mm:SS.FFF")
'14:30'
>>> Time(14, 30, 0, nanosecond=500_000_000).format("hh:mm:SS.FFF")
'14:30:00.5'

Fractional seconds:

  • f specifies the exact number of digits. fff always writes 3 digits.

  • F specifies the maximum digits, with trailing zeros trimmed. FFF writes 1–3 digits, or nothing if the value is zero (and also trims a preceding .).

>>> Time(14, 30, 5, nanosecond=120_000_000).format("hh:mm:ss.fff")
'14:30:05.120'
>>> Time(14, 30, 5, nanosecond=120_000_000).format("hh:mm:ss.FFF")
'14:30:05.12'
>>> Time(14, 30, 5).format("hh:mm:ss.FFF")
'14:30:05'

Offset and timezone specifiers

See Timezones for background on timezones, offsets, and abbreviations.

Offset specifiers (x/X):

Pattern

Meaning

Example output

Parse support

x

Offset hours only

+02

xx

Offset hours+minutes, compact

+0230

xxx

Offset hours:minutes

+02:30

xxxx

Compact, optional seconds

+023045

xxxxx

With colons, optional seconds

+02:30:45

XXXXXX

Same as xxxxxx, but Z for zero offset

+02:30, Z

Lowercase x always produces a numeric offset. Uppercase X substitutes Z when the offset is exactly zero. For widths 4 and 5, seconds are only displayed when non-zero.

Timezone specifiers:

Pattern

Meaning

Example output

Parse support

VV

IANA timezone ID

Europe/Paris

zz

Timezone abbreviation

CET, CEST

❌ format only

Choosing between x and X

Use uppercase X when you want Z for zero offset (e.g. Instant formatting). Use lowercase x when you always want a numeric offset (e.g. OffsetDateTime formatting). Both accept Z when parsing with uppercase X.

Note

zz (timezone abbreviation) is only supported for formatting—abbreviations are ambiguous and cannot be reliably used for parsing. Use VV (IANA timezone ID) instead. See Timezones for why abbreviations are unreliable.

>>> ZonedDateTime(2024, 7, 15, 14, 30, tz="Europe/Paris").format(
...     "YYYY-MM-DD hh:mm zz"
... )
'2024-07-15 14:30 CEST'
>>> ZonedDateTime.parse(
...     "2024-07-15 14:30+02:00[Europe/Paris]",
...     format="YYYY-MM-DD hh:mmxxx'['VV']'",
... )
ZonedDateTime("2024-07-15 14:30:00+02:00[Europe/Paris]")

Supported specifiers per type

Type

Date

Time

x/X

VV/zz

Date

Time

PlainDateTime

OffsetDateTime

ZonedDateTime

Instant

Literal text

Common non-letter characters (:, -, /, ., ,, ;, _, (, ), digits, spaces, and other ASCII punctuation) are treated as literals by default:

>>> Date(2024, 3, 15).format("YYYY/MM/DD")
'2024/03/15'

Letters must be quoted with single quotes to be used as literals. This prevents accidental use of reserved characters and keeps options open for future specifiers:

>>> Date(2024, 3, 15).format("YYYY'xx'MM")
'2024xx03'

To include a literal single quote, use '':

>>> Date(2024, 3, 15).format("YYYY''MM")
"2024'03"

Restrictions

  • ASCII-only: Pattern strings must contain only ASCII characters. Non-ASCII characters raise ValueError.

  • Reserved characters: <, >, [, ], {, }, and # are reserved for future use and cannot appear unquoted.

  • No duplicate fields: A pattern cannot contain two specifiers that set the same value. For example, MM and MMM both set the month, so "DD MM MMM YYYY" is invalid.

Parsing requirements

Some types require specific fields in the parse pattern:

All types that include date fields require YYYY, MM, and DD.

Comparison with strftime

The parse_strptime() methods on OffsetDateTime and PlainDateTime are deprecated in favor of parse(). Here’s a migration guide:

strftime

Pattern

Notes

%Y

YYYY

%y

YY

Format only

%m

MM

%b

MMM

%B

MMMM

%d

DD

%a

ddd

%A

dddd

%H

hh

Note: hh = 24-hour

%I

ii

Note: ii = 12-hour

%M

mm

%S

ss

%f

ffffff

microseconds (6 digits)

%p

aa

%z

xxxx

XXXX for Z-style

%:z

xxxxx

XXXXX for Z-style

%Z

Abbreviations are not supported for parsing. See Timezones.