HOWTO · Python

Convert a Python Datetime to a String With Milliseconds

Convert a Python datetime to an ISO-style or custom string with exactly three millisecond digits, while preserving timezone offsets and avoiding slicing errors.

On this page

Use value.isoformat(timespec="milliseconds") when an ISO-style result is suitable. Use strftime() with %f and remove its last three digits when you need a custom layout. Both approaches truncate microseconds to milliseconds rather than rounding them.

Formatting changes the representation, not the datetime value itself. The examples use fixed inputs so that their output can be verified exactly; replace those constructors with a value from your application after choosing the required layout. If the receiving system expects an offset, make the value timezone-aware before formatting it.

Use isoformat() for Three-Digit Millisecond Precision

datetime.isoformat() is the clearest choice for an ISO 8601-style string. The timespec="milliseconds" argument always emits exactly three fractional-second digits. It is available in Python 3.6 and later.

Pass the lowercase plural string "milliseconds" exactly. Other supported timespec values select different precision levels, while an unknown value raises ValueError. This explicit parameter is preferable to relying on the default "auto" behavior when a downstream schema requires a consistent field width.

The following deterministic example uses an aware datetime, so the result also demonstrates that isoformat() retains the UTC offset:

"""Show the recommended ISO-style millisecond formatting behavior."""

from datetime import datetime, timezone


value = datetime(2026, 9, 16, 12, 34, 56, 789654, tzinfo=timezone.utc)
print(value.isoformat(timespec="milliseconds"))

Output:

2026-09-16T12:34:56.789+00:00

The six-digit microsecond value 789654 becomes .789. Python does not round the value to .790; the timespec controls which components appear, and excluded time components are truncated.

Use an aware datetime when the string must identify an actual instant across systems. A naive value has no UTC offset, so its formatted text cannot by itself distinguish a local time from UTC or another zone.

isoformat() preserves the offset already attached to the object; it does not convert the value to UTC. Normalize the datetime first if your data contract requires UTC. Conversely, omit timezone conversion only when the consumer deliberately expects a local wall-clock value.

Use strftime() for a Custom Layout

Use strftime() when you need to control the order, separators, or other fields. The %f directive produces six microsecond digits. Slicing with [:-3] deliberately removes the final three digits and leaves millisecond precision:

"""Show millisecond precision in a custom datetime string format."""

from datetime import datetime


value = datetime(2026, 9, 16, 12, 34, 56, 789654)
print(value.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3])

Output:

2026-09-16 12:34:56.789

This slicing is safe because %f always supplies a zero-padded six-digit field, even when the original value has no microseconds. Keep .%f at the end of the format before using [:-3]; otherwise, the slice could remove characters from another field.

The operation truncates rather than rounds. For example, 789999 microseconds still becomes 789 milliseconds. If a specification requires rounding, round the datetime before formatting and handle a possible carry into the next second; simply slicing %f cannot implement that rule.

The import style determines how you call the class. With from datetime import datetime, call datetime.now(). If you instead write import datetime, call datetime.datetime.now().

Understand str() and Slicing Boundaries

Plain str(value) is equivalent to value.isoformat(" "). Its default timespec="auto" omits the fractional field when microsecond is zero and otherwise emits all six microsecond digits. Therefore, str(value) does not guarantee a three-digit millisecond field, and blindly applying [:-3] is unsafe.

This variable-width behavior is useful for informal display but unsuitable for a fixed-width field. Checking for a decimal point before slicing would avoid corrupting the seconds, yet it would still duplicate logic already handled by isoformat(timespec="milliseconds") and would require extra care for timezone suffixes.

This boundary example also shows that millisecond output is truncated at 999999 microseconds and that the singular value "millisecond" is invalid:

"""Expose truncation, zero-microsecond slicing, and invalid-timespec boundaries."""

from datetime import datetime


almost_next_second = datetime(2026, 9, 16, 12, 34, 56, 999999)
without_fraction = datetime(2026, 9, 16, 12, 34, 56)

print(almost_next_second.isoformat(timespec="milliseconds"))
print(str(without_fraction))
print(str(without_fraction)[:-3])

try:
    without_fraction.isoformat(timespec="millisecond")
except ValueError as error:
    print(f"{type(error).__name__}: {error}")

Output:

2026-09-16T12:34:56.999
2026-09-16 12:34:56
2026-09-16 12:34
ValueError: Unknown timespec value

The third line is not a valid millisecond conversion: it removes :56 because the input has no fractional field. Prefer isoformat(timespec="milliseconds"), or use a format containing %f, instead of slicing the variable-length result of str().

Choose the Appropriate Method

Use isoformat(timespec="milliseconds") for machine-readable timestamps and standard interchange. It handles zero microseconds, truncation, and timezone offsets without manual string manipulation. Use strftime() plus [:-3] when a consumer requires a custom layout.

Choose based on the output contract rather than convenience: isoformat() supplies a standardized date-time shape, strftime() supplies a caller-defined shape, and str() supplies a human-friendly default with variable fractional precision. In all cases, document whether a missing offset means local time, UTC by convention, or an unknown timezone.

These methods format an existing datetime; they do not convert it to milliseconds since the Unix epoch or parse a string into a datetime. Also remember that three displayed digits describe millisecond precision, not necessarily the accuracy of the clock or the stored source value.