---
myst:
html_meta:
description: >-
Reference for patterns: date, time, offset, and time zone specifiers,
literal text rules, parsing requirements, migrating patterns in 0.11,
and how it differs from strftime.
---
(pattern-format)=
# Pattern format
```{eval-rst}
.. currentmodule:: whenever
```
Patterns allow you to format datetime values into strings and parse
strings into datetime values, using a pattern string that describes the
expected format. The canonical full datetime pattern is
`YYYY-MM-DD HH:mm:ss`.
## Quick example
```python
>>> from whenever import Date, Time, OffsetDateTime, hours
>>> Date(2024, 3, 15).format("YYYY/MM/DD")
'2024/03/15'
>>> Date.parse("2024/03/15", pattern="YYYY/MM/DD")
Date("2024-03-15")
>>> OffsetDateTime(2024, 3, 15, 14, 30, offset=hours(2)).format(
... "EEE, DD MMM YYYY HH:mm:ssxxx"
... )
'Fri, 15 Mar 2024 14:30:00+02:00'
```
The same six types—{class}`Date`, {class}`Time`, and the four datetime
types—have a `__format__` that takes a pattern, so `f"{d:YYYY/MM/DD}"` works
too, and an empty format spec gives `str()`. The other types have no patterns
and keep `object.__format__`, which accepts only the empty spec: a pattern
needs a complete calendar date or clock time on both sides, which only these
six types hold.
## 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
| Symbol | Meaning | Pattern | Example output |
|:---------|:---------------------------|:---------------|:--------------|
| `Y` | year | `YY` [^1]
`YYYY` | `24`
`2024` |
| `M` | month | `M`
`MM`
`MMM`
`MMMM` | `3`
`03`
`Mar`
`March` |
| `D` | day of month | `D`
`DD` | `5`
`05` |
| `E` | day of week [^2] | `EEE`
`EEEE` | `Fri`
`Friday` |
Month and weekday names are English, whatever the locale.
:::{admonition} Why are Whenever's patterns different?
:class: important
Whenever deliberately avoids classic datetime-pattern footguns. `Y` is always
the calendar year, never the ISO week-numbering year. `H` is the 24-hour clock,
as in almost all other datetime libraries, while the distinct `i` is the
12-hour clock and makes a missing AM/PM specifier detectable. `D` is always the
day of the month; day-of-year is not supported. Familiar but unsupported
letters fail instead of silently acquiring a different meaning.
:::
### Time specifiers
| Symbol | Meaning | Pattern | Example output |
|:---------|:---------------------------|:---------------|:--------------|
| `H` | hour (24-hour clock) | `H`
`HH` | `4`
`04` |
| `i` | hour (12-hour clock) | `i`
`ii` | `4`
`04` |
| `m` | minute | `m`
`mm` | `5`
`05` |
| `s` | second | `s`
`ss` | `5`
`05` |
| `[...]` | optional seconds [^3] | `[ss]`
`[:ss]`
`[:ss.fff]`
`[:ss.FFF]` | `05`, (omitted)
`:05`, (omitted)
`:05.123`, (omitted)
`:05.12`, (omitted) |
| `f` | fractional seconds, exact digits | `f`
`ff`
`fff`
...
`fffffffff` | `1`
`12`, `00`
`123`, `400`
...
`123456789`, `374930000` |
| `F` | fractional seconds, trimmed [^4] | `F`
`FF`
`FFF`
...
`FFFFFFFFF` | `1`
`12`, (omitted)
`123`, `4`
...
`123456789`, `37493` |
| `a` | AM/PM [^5] | `a`
`aa` | `P`
`PM` |
:::{admonition} Optional seconds
:class: hint
Brackets have one limited use immediately after fixed-width `mm`: optional
seconds written as `[ss]` or `[:ss]`, followed optionally by `.` and 1–9 `f`
or `F` characters. Brackets are not general-purpose optional groups, and the
colon is the only supported separator.
The whole group is omitted when both seconds and nanoseconds are zero. Otherwise,
the separator and two zero-padded second digits are emitted. Lowercase `f`
requires exactly that many fractional digits; uppercase `F` trims trailing
zeroes and omits the decimal point when the fraction is empty.
```python
>>> 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'
>>> Time(14, 30, 5).format("HH:mm[ss]")
'14:3005'
```
The group boundary must remain unambiguous without backtracking.
Separator-free `[ss...]` therefore cannot be followed by an element that
starts with a digit, and `[:ss...]` cannot be followed by another colon.
Specifiers that may be empty are also rejected as followers when they make the
boundary ambiguous. A trimmed optional fraction cannot be followed by a
literal period.
:::
### Offset and time zone specifiers
See {ref}`timezones-explained` for background on time zones, offsets, and abbreviations.
| Symbol | Meaning | Pattern | Example output |
|:---------|:---------------------------|:---------------|:--------------|
| `x` | Numeric offset; precision depends on width | `x`
`xx`
`xxx`
`xxxx`
`xxxxx` | `+02`
`+0230`
`+02:30`
`+023045`
`+02:30:45` |
| `X` | Numeric offset, with `Z` for zero offset; precision depends on width | `X`
`XX`
`XXX`
`XXXX`
`XXXXX` | `+02`
`+0230`
`+02:30`
`+023045`
`+02:30:45` or `Z` when zero |
| `V` | time zone ID | `VV` | `Europe/Paris` |
| `z` | Time zone abbreviation [^6] | `zz` | `CET`, `CEST` |
For `x` and `X`, widths `xx` and `xxx` round offset seconds to the nearest
minute, with half values rounded away from zero. Widths `x` and `X` apply the
same rounding but require the result to be a whole number of hours; otherwise
formatting raises {class}`ValueError`. Widths `xxxx` and `xxxxx` include offset
seconds when nonzero and therefore preserve them exactly. An offset that
rounds to 24 hours, such as `+23:59:45` written as `xxx`, raises
{class}`ValueError`.
When parsing a {class}`ZonedDateTime`, an offset without seconds is matched
against the time zone offset rounded in the same way, and an offset that
includes seconds must match exactly. `Z` isn't matched against the time zone:
it states a UTC instant, which is converted to the time zone.
`"14:30Z Europe/Paris"` parses as 16:30+02:00, where `"14:30+00:00 Europe/Paris"`
raises {class}`InvalidOffsetError`.
`VV` requires a time zone ID. Formatting a time zone without one raises
{class}`ValueError`.
```{admonition} Choosing between x and X
:class: hint
Use uppercase `X` when you want `Z` for zero offset
(e.g. {class}`Instant` formatting).
When parsing, `X` also accepts a lowercase `z`, as ISO 8601 parsing does.
Use lowercase `x` when you always want a numeric offset
(e.g. {class}`OffsetDateTime` formatting).
```
```python
>>> 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]",
... pattern="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` |
|:------------------|:----:|:----:|:--------:|:----:|
| {class}`Date` | ✅ | ❌ | ❌ | ❌ |
| {class}`Time` | ❌ | ✅ | ❌ | ❌ |
| {class}`PlainDateTime` | ✅ | ✅ | ❌ | ❌ |
| {class}`OffsetDateTime`| ✅ | ✅ | ✅ | ❌ |
| {class}`ZonedDateTime` | ✅ | ✅ | ✅ | ✅ |
| {class}`Instant` | ✅ | ✅ | ✅ | ❌ |
## Literal text
Common non-letter characters (`:`, `-`, `/`, `.`, `,`, `;`,
`_`, `(`, `)`, digits, spaces, tabs, line feeds (`\n`), and other ASCII
punctuation) are treated as literals by default:
```python
>>> 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:
```python
>>> Date(2024, 3, 15).format("YYYY'xx'MM")
'2024xx03'
```
To include a literal single quote, use `''`,
inside a quoted run or outside one:
```python
>>> Date(2024, 3, 15).format("YYYY''MM")
"2024'03"
>>> Date(2024, 3, 15).format("'it''s' YYYY")
"it's 2024"
```
### Restrictions
- **ASCII-only**: Pattern strings must contain only ASCII characters.
Non-ASCII characters raise ``ValueError``.
- **Reserved characters**: `<`, `>`, `[`, `]`, `{`, `}`, and `#` cannot
appear unquoted, except for brackets used by the optional-seconds syntax.
- **No duplicate specifiers**: 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.
- **No AM/PM with the 24-hour clock**: `H` or `HH` together with `a` or `aa`
raises {class}`ValueError`. Use `i` or `ii` with AM/PM.
## Parsing requirements
Parsed input strings must contain only ASCII characters.
Variable-width numeric specifiers must be separated from following digits. The
same rule applies to specifiers that omit optional digits, such as trimmed
fractions and the optional seconds of an `xxxx` offset. The optional seconds
of an `xxxxx` offset start with a colon instead, so `xxxxx` can't be followed
by a colon. `VV` must be
the final specifier or be followed by a literal delimiter that cannot occur in a
time zone ID. A dotted trimmed fraction cannot be followed by another
dot, and when parsing it takes the dot only together with a digit:
`12:30:05.` does not match `HH:mm:ss.FFF`.
Ambiguous patterns raise {class}`ValueError` when compiled.
Some types require specific specifiers in the parse pattern:
- {meth}`OffsetDateTime.parse() ` requires an offset (`x`/`X`)
- {meth}`ZonedDateTime.parse() ` requires `VV` (time zone ID).
An offset (`x`/`X`) is optional but recommended for DST disambiguation.
- {meth}`Instant.parse() ` requires an offset (`x`/`X`)
All types that include a date require a year, a month, and a day, in any of
their spellings; `DD MMM YYYY` is complete.
Month names, weekday names, and AM/PM are matched case-insensitively:
`jan`, `JANUARY`, and `pm` all parse. The 12-hour clock `ii` accepts hours
1 to 12 only.
A second value of ``60`` (leap second) is accepted and normalized to ``59``.
See [](faq-leap-seconds) for details.
## Migrating patterns in 0.11
Version 0.11 accepts both the final spellings and the previous forms. Previous
forms emit {class}`WheneverDeprecationWarning` on every `format()` or `parse()`
call and will be rejected in 1.0.
| Previous pattern | Replacement |
|:-----------------|:------------|
| `h` | `H` |
| `hh` | `HH` |
| `:SS` | `[:ss]` |
| `:SS.FFF` | `[:ss.FFF]` |
| separator-free `SS` | `[ss]` |
The same bracketed spelling applies to exact fractions, such as replacing
`:SS.fff` with `[:ss.fff]`. Keeping the separator inside the brackets makes
it disappear with the seconds.
## Comparison with strftime
The following table maps common `strftime` directives to Whenever patterns:
| strftime | Pattern | Notes |
|:---------|:--------|:------|
| `%Y` | `YYYY`| |
| `%y` | `YY` | Format only |
| `%m` | `MM` | |
| `%b` | `MMM` | |
| `%B` | `MMMM`| |
| `%d` | `DD` | |
| `%a` | `EEE` | |
| `%A` | `EEEE`| |
| `%H` | `HH` | 24-hour clock |
| `%I` | `ii` | 12-hour clock |
| `%M` | `mm` | |
| `%S` | `ss` | |
| `%f` | `ffffff`| microseconds (6 digits) |
| `%p` | `aa` | |
| `%z` | `xxxx` | `XXXX` for Z-style |
| `%:z` | `xxxxx` | `XXXXX` for Z-style |
| `%Z` | `zz` | Format only. See {ref}`timezones-explained`. |
[^1]: `YY` is only supported for formatting. When parsing, use `YYYY` to avoid ambiguity.
[^2]: During parsing, weekday names are validated against the parsed date. A mismatch raises ``ValueError``.
[^3]: The complete bracketed group is omitted when both seconds and nanoseconds are zero.
[^4]: Omitted when the value is zero, with preceding `.` also omitted.
[^5]: AM/PM is determined by the hour value. Using `i`/`ii` without `a`/`aa` emits a warning, since a value such as `03:00` could mean 3 AM or 3 PM.
[^6]: Time zone abbreviations are not supported for parsing: an abbreviation names more than one time zone. Use `VV` (time zone ID) instead. See {ref}`timezones-explained` for details.