StackPractices
beginner By Mathias Paulenko

Date Formatting in Python, JavaScript & Java

How to parse, format, and display dates across timezones in Python, JavaScript, and Java, with a per-language directive cheat sheet.

Topics: data

Overview

Formatting turns a Date, datetime, or Instant into something a person can read; parsing does the reverse. Both look trivial and both break in the same two places: ambiguous local formats (02/03/2026 means different days in the US and in Europe) and timezone handling.

The rule that prevents most production bugs: store and transmit dates as UTC instants (ISO 8601 or epoch), and convert to the user’s locale and timezone only at the presentation layer.

When to Use

Use this recipe when you need to:

  • Serialize dates to JSON or XML for an REST API, where ISO 8601 is the de facto standard.
  • Render dates in the UI so each reader sees their own format: 10/22/2026 for US users, 22/10/2026 for Spanish ones.
  • Parse dates entered by users or loaded from files. See Data Validation for sanitizing that input.
  • Convert one instant between timezones, such as showing a deploy time in each teammate’s zone.
  • Write precise timestamps to logs and audit trails. See Logging for structured patterns.

Skip manual string formatting when you need relative times (“3 hours ago”) or calendar math (next business day, every second Tuesday). Dedicated libraries handle those better; see Variants.

Solution

Python

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# Current instant, stored in UTC
now_utc = datetime.now(timezone.utc)
print(now_utc.isoformat())  # 2026-06-10T14:30:00+00:00

# Format for display
print(now_utc.strftime("%Y-%m-%d %H:%M:%S"))  # 2026-06-10 14:30:00
print(now_utc.strftime("%A, %B %d, %Y"))      # Wednesday, June 10, 2026

# Parse an ISO 8601 string and convert zone
dt = datetime.fromisoformat("2026-06-10T14:30:00+00:00")
madrid = dt.astimezone(ZoneInfo("Europe/Madrid"))
print(madrid.strftime("%Y-%m-%d %H:%M %Z"))  # 2026-06-10 16:30 CEST

# Parse a local format explicitly
dt2 = datetime.strptime("10/06/2026", "%d/%m/%Y")  # June 10, not Oct 6

JavaScript

const now = new Date();

// ISO 8601, always UTC
console.log(now.toISOString()); // 2026-06-10T14:30:00.000Z

// Locale-aware formatting
console.log(now.toLocaleString('en-US', {
  dateStyle: 'full',
  timeStyle: 'short',
  timeZone: 'America/New_York',
})); // Wednesday, June 10, 2026 at 10:30 AM

// Intl.DateTimeFormat, created once and reused
const fmt = new Intl.DateTimeFormat('de-DE', {
  year: 'numeric', month: '2-digit', day: '2-digit',
  hour: '2-digit', minute: '2-digit',
  timeZone: 'Europe/Berlin',
});
console.log(fmt.format(now)); // 10.06.2026, 16:30

// Parse an ISO string
const parsed = new Date('2026-06-10T14:30:00Z');
console.log(parsed.toISOString());

Java

import java.time.*;
import java.time.format.DateTimeFormatter;

ZonedDateTime nowUtc = ZonedDateTime.now(ZoneOffset.UTC);
System.out.println(nowUtc.format(DateTimeFormatter.ISO_INSTANT));

DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
System.out.println(nowUtc.format(formatter)); // 2026-06-10 14:30:00 UTC

ZonedDateTime parsed = ZonedDateTime.parse("2026-06-10T14:30:00Z");
ZonedDateTime tokyo = parsed.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println(tokyo.format(formatter)); // 2026-06-10 23:30:00 JST

How It Works

An instant is one fixed point on the global timeline. Wall time is different: it’s whatever a clock shows in a particular zone, and it drifts with daylight saving. 2026-06-10T14:30:00Z names an instant without ambiguity; 14:30 alone could sit anywhere on the timeline.

The other distinction that trips people up is offset versus timezone. +02:00 is only a fixed distance from UTC, while Europe/Madrid is a named zone in the IANA database that carries rules of its own, including daylight saving: UTC+1 in winter, UTC+2 in summer. Freeze the offset into your data and you’re off by an hour for half the year.

DST is where the traps live. At spring-forward, a local time is skipped and 02:30 never happens that day. At fall-back, a local time happens twice, so 01:30 names no single instant. UTC sidesteps both problems.

Date handling flow: parse input, store as UTC ISO 8601, format at presentation layer

Format Pattern Cheat Sheet

Pattern letters aren’t portable between languages, and case means different things in each. In Java, MM means month while mm means minutes; Python keeps the same letters in a different convention: %m for month, %M for minutes. Swapping them produces silently wrong output.

Python strftime / strptime

DirectiveMeaningExample
%Y4-digit year2026
%mMonth (01-12)06
%dDay of month (01-31)10
%AFull weekday nameWednesday
%BFull month nameJune
%HHour 24h (00-23)14
%IHour 12h (01-12)02
%MMinute30
%SSecond00
%pAM or PMPM
%zUTC offset+0000
%ZTimezone nameUTC
%jDay of year161

Java DateTimeFormatter

PatternMeaningExample
yyyyYear2026
MMMonth number06
MMMShort month nameJun
MMMMFull month nameJune
ddDay of month10
EEEEFull weekdayWednesday
HHHour 0-2314
mmMinute30
ssSecond00
XXXISO 8601 offsetZ or +02:00
VVIANA timezone IDEurope/Madrid
zShort timezone nameCEST

JavaScript Intl.DateTimeFormat

OptionValuesExample output
dateStylefull, long, medium, shortWednesday, June 10, 2026
timeStylefull, long, medium, short2:30 PM
yearnumeric, 2-digit2026 or 26
monthnumeric, 2-digit, long, short, narrow6, 06, June, Jun, J
daynumeric, 2-digit10
weekdaylong, short, narrowWednesday, Wed, W
hour, minute, secondnumeric, 2-digit14, 30, 00
hour12true, false2:30 PM vs 14:30
timeZoneIANA IDEurope/Madrid
timeZoneNameshort, longCEST

The same output in each language

OutputPythonJavaJavaScript
2026-06-10%Y-%m-%dyyyy-MM-dddateStyle: 'short' (locale dependent)
2026-06-10T14:30:00Zisoformat()ISO_INSTANTtoISOString()
Wednesday, June 10, 2026%A, %B %d, %YEEEE, MMMM d, yyyydateStyle: 'full' in en-US
14:30%H:%MHH:mmtimeStyle: 'short' in en-GB
2:30 PM%I:%M %ph:mm ahour12: true in en-US

Variants and Alternatives

  • Python: the stdlib datetime plus zoneinfo (Python 3.9+) covers most cases. python-dateutil handles fuzzy parsing like “next Friday”. Arrow and Pendulum wrap the same engine in a friendlier API.

  • JavaScript: Intl.DateTimeFormat ships in every modern runtime, costs 0 KB, and knows every IANA zone. Reach for date-fns or Day.js when you need parsing and arithmetic, and Luxon when the app is timezone-heavy. Avoid moment in new code: it’s mutable, weighs ~70 KB minified, and sits in maintenance mode.

  • Java: always java.time. java.util.Date and SimpleDateFormat are mutable and not thread-safe.

  • Performance note: constructing a formatter is the expensive part. DateTimeFormatter instances are immutable and thread-safe, so cache and reuse them. The same applies to Intl.DateTimeFormat inside a hot loop.

  • Runnable companion: three working scripts (Python, JavaScript, Java) in the companion repo with the examples from this page.

Best Practices

  • Store every date as a UTC instant and convert to local time only when rendering for the user.
  • On the wire, always send ISO 8601 rather than a locale-specific string. A value such as 2026-06-10T14:30:00Z means the same instant to a Python backend, a Node API, and a Java service.
  • Send the offset or timezone in API responses so clients can render local time correctly.
  • Prefer IANA IDs (America/New_York) over abbreviations (EST), which ignore DST and are ambiguous (CST names three different zones).
  • Never build or parse dates by concatenating strings or with regular expressions; use the platform parser.

Common Mistakes

  • Storing wall time with no zone: a 2026-06-10 14:30 row in the database can’t be reconstructed once servers or users move across zones.
  • Parsing non-ISO strings with Date.parse(): each engine treats 10/06/2026 differently, so pass an explicit format or use a parsing library.
  • MM vs mm confusion: month versus minutes is the most common silent bug in Java DateTimeFormatter patterns.
  • Formatting in the server’s timezone: the API responds in whatever zone the server runs in, and the user usually reads another. Format at the edge.
  • Assuming every local time exists: scheduling something at 02:30 fails on spring-forward days in zones that observe DST.

See Also

Frequently Asked Questions

Should I store timestamps or formatted strings in my database?

Store instants in a native TIMESTAMP WITH TIME ZONE column (or epoch seconds), never formatted strings. Strings lose the instant and sort wrong across zones. See Database Transactions for related integrity patterns.

How do I handle daylight saving time?

Everything stays in UTC internally — convert with IANA IDs (Europe/Madrid) at display time. Baking +02:00 into stored data or code breaks the moment DST flips.

What is the difference between a timezone and a UTC offset?

+02:00 is an offset: a fixed distance from UTC that never changes. Europe/Madrid is a timezone: a named region whose offset jumps between +1 and +2 as DST rules kick in. Store instants and format with timezones rather than bare offsets.

What is the difference between toISOString() and toUTCString()?

JSON APIs and databases expect ISO 8601 — and toISOString() produces exactly that (2026-06-10T14:30:00.000Z). toUTCString() returns the older RFC 7231 string (Wed, 10 Jun 2026 14:30:00 GMT) that appears in HTTP headers.

How do I format a date in the user's locale without a library?

new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format(date) picks up the runtime's locale automatically. If the user chose a zone in settings, pass it through as an explicit timeZone.