Back to Blog
Python

Python F-String Datetime Formatting

Learn how to format datetime objects directly in Python f-strings using strftime format codes, timezone handling, and practical examples.

f-stringsdatetime formattingstrftimeformat specifierstimezone handling
A clock and calendar icon with Python f-string syntax showing datetime formatting.

Formatting a datetime object directly in an f-string keeps datetime formatting close to the output. Instead of calling strftime() separately and then interpolating the result, you can apply the same strftime() format codes inside the f-string itself.

The Basic Syntax for Datetime Formatting

An f-string expression that formats a datetime uses a colon (:) followed by a format specifier. The specifier uses the same % codes as strftime(). For example, to display a date as YYYY-MM-DD, you write:

from datetime import datetime now = datetime.now() print(f'{now:%Y-%m-%d}')

The colon separates the expression from the format specifier, and the specifier is interpreted exactly as it would be in strftime(). The %Y directive is replaced by the year, %m by the zero-padded month, and %d by the zero-padded day.

You can also combine literal text with the specifier. For instance, to produce a readable timestamp:

print(f'{now:%A, %B %d, %Y at %H:%M}')

This yields something like Friday, December 13, 2024 at 14:30. The format codes %A and %B produce the full weekday and month names, while %H and %M give the hour and minute in 24-hour form.

Using strftime Format Codes Inside F-Strings

Every format code accepted by datetime.strftime() is available inside an f-string. This includes platform-dependent codes such as %x for the locale's date representation and %c for the locale's date and time. The output matches datetime.strftime() on the same platform.

For example, to format a datetime with the timezone offset and name, you can use %z and %Z:

from datetime import datetime, timezone utc_now = datetime.now(timezone.utc) print(f'{utc_now:%Y-%m-%d %H:%M %z %Z}')

This prints the UTC time with the offset +0000 and the timezone name UTC. The same codes work for aware datetimes in any timezone.

One advantage of using f-strings is that you can combine multiple datetime objects and other variables in a single expression without intermediate variables. For example:

start = datetime(2024, 1, 1, 9, 30) end = datetime(2024, 1, 1, 17, 45) print(f'Shift: {start:%H:%M} - {end:%H:%M}')

This avoids creating separate strings for each time and then concatenating them.

Common Datetime Format Patterns

Certain format patterns appear frequently in logs, filenames, and user-facing output. The following table shows a few typical patterns and their f-string equivalents.

Intended outputFormat specifierExample f-string
ISO date%Y-%m-%df'{dt:%Y-%m-%d}'
24-hour time%H:%M:%Sf'{dt:%H:%M:%S}'
Full timestamp%Y-%m-%d %H:%Mf'{dt:%Y-%m-%d %H:%M}'
Weekday and date%A, %B %df'{dt:%A, %B %d}'
Microseconds%ff'{dt:%f}'

When you need a filename-safe timestamp, a common pattern is:

from datetime import datetime now = datetime.now() filename = f'report_{now:%Y%m%d_%H%M%S}.txt'

This produces report_20241213_143050.txt. The format specifier uses no separators between the date and time components, which is useful for sorting or unique naming.

Handling Timezones and UTC Offsets

F-strings handle timezone-aware datetimes correctly when you use the appropriate format codes. The %z code outputs the UTC offset in the form +HHMM or -HHMM, and %Z outputs the timezone name if available. For a datetime created with timezone.utc, the name is UTC. For a datetime using the standard-library zoneinfo module (Python 3.9+; backports.zoneinfo provides the same API on older versions), %Z returns the zone's abbreviation for that date and time, such as CET or CEST for Europe/Berlin.

from datetime import datetime from zoneinfo import ZoneInfo berlin = datetime.now(ZoneInfo('Europe/Berlin')) print(f'{berlin:%Y-%m-%d %H:%M %z %Z}')

This prints the local time in Berlin with the correct offset and zone abbreviation. The exact abbreviation from %Z depends on the platform's C library; on Windows it may be a localized abbreviation. If you need the stable IANA identifier for a ZoneInfo timezone, use berlin.tzinfo.key instead of relying on %Z.

When you format a naive datetime (one without timezone information), %z and %Z produce empty strings. This can be surprising if you expect an offset. Always check whether your datetime is aware before formatting it with timezone codes.

Locale-Sensitive Formatting and Pitfalls

Format codes like %x, %X, and %c depend on the current locale. The output can change when the locale changes, which is useful for internationalization but can also lead to inconsistent logs or filenames if the locale is not controlled. For example, %x might produce 12/13/24 in the US locale and 13.12.2024 in a German locale.

If you need a locale-independent format, stick to numeric codes like %Y-%m-%d and %H:%M:%S. These are always unambiguous and do not change with locale settings. When you deliberately want locale-aware output, set the locale explicitly using locale.setlocale() before formatting, and be aware that this affects the entire process.

Another common pitfall is using %f for microseconds. The %f code outputs microseconds as a zero-padded six-digit number. If you need milliseconds, format the full timestamp and then slice the resulting string:

now = datetime.now() ts = f'{now:%H:%M:%S.%f}' print(ts[:-3])

This drops the last three digits.

Performance and Maintainability Considerations

Formatting a datetime with an f-string is essentially equivalent to calling strftime(). The format specifier is interpreted when the f-string is evaluated, so the choice between f'{dt:%Y-%m-%d}' and dt.strftime('%Y-%m-%d') is normally about readability. If you are formatting many timestamps and profiling shows a bottleneck, a named format string can keep the format in one place, but it does not avoid interpreting the specifier.

From a maintainability perspective, f-strings keep the format specifier adjacent to the value being formatted. This makes it easier to see the output structure at a glance. However, if the same format is used in many places, a named constant can prevent duplication:

LOG_TIMESTAMP = '%Y-%m-%d %H:%M:%S' print(f'{now:{LOG_TIMESTAMP}}')

This works because the format specifier can itself be an expression inside the braces. That pattern is useful when you need to reuse a format across multiple calls or change it in one place.

Edge Cases and Compatibility Notes

datetime supports years from 1 to 9999. The %Y directive prints the full year, but for years before 1000 it may not be zero-padded on every platform. If you need a fixed four-digit year, format the year attribute explicitly:

print(f'{dt.year:04d}-{dt.month:02d}-{dt.day:02d}')

This avoids relying on platform-specific %Y behavior.

F-strings do not require a separate formatter import; the datetime class supports the __format__ protocol that f-strings use. Python's f-string support for datetime format specifiers was introduced in Python 3.6, along with f-strings themselves. The format specifier behavior matches strftime() in the same Python version.

One compatibility issue is that %s (Unix timestamp) is not a portable strftime() code. It may work on some platforms, but it is not guaranteed across Python implementations or operating systems. If you need a portable Unix timestamp, use int(dt.timestamp()) instead.

Finally, when you format an aware datetime with a timezone that has a non-integer offset (such as +05:30), the %z code outputs +0530 without a colon. If you need the colon-separated form, you can reformat the offset manually:

offset = dt.strftime('%z') # e.g., '+0530' formatted = f'{offset[:3]}:{offset[3:]}'

This gives +05:30 instead of +0530 when required by a protocol or API.

Python F-String Datetime Formatting: Practical Usage and Code | RYUSLOG DEV