Datetime in Python: Working with Dates and Times

The datetime rules that prevent real bugs: why naive and aware datetimes cannot be compared, how to attach and convert time zones with zoneinfo, parsing ISO strings and custom formats, formatting for people, and why adding one day is not always 24 hours.

GloryTecks Team

Training & Placement Faculty

6 May 2026 5 min read

Last updated: 24 September 2026

Share:

Date bugs are rarely crashes. They are an order timestamped five and a half hours early, a report that double-counts the hour when clocks go back, a "daily" job that runs twice. Nearly all of them come from one question asked too late: which time zone is this? Python's datetime module answers it well, provided you know the difference between two kinds of datetime.

Naive and aware: the distinction everything depends on

A naive datetime has no time zone attached. It is just a calendar date and a wall-clock time, and Python cannot know which instant it means. An aware datetime carries a time zone, so it identifies one precise moment (Python docs: datetime).

python
from datetime import date, datetime, timedelta, timezone
from zoneinfo import ZoneInfo

IST = ZoneInfo("Asia/Kolkata")
naive = datetime(2026, 3, 1, 9, 30)
aware = datetime(2026, 3, 1, 9, 30, tzinfo=IST)

print(naive.tzinfo, aware.utcoffset())
try:
    naive < aware
except TypeError as exc:
    print(exc)
text
None 5:30:00
can't compare offset-naive and offset-aware datetimes

That TypeError is Python protecting you: it refuses to guess. The rule that follows is simple. Make datetimes aware as soon as they enter your program, and keep them aware.

Time zones with zoneinfo

Since Python 3.9, the standard library's zoneinfo module provides the IANA time zone database: names like Asia/Kolkata, Europe/London and America/New_York, with their full history of offset and daylight-saving changes (Python docs: zoneinfo). On Windows, which has no system copy of that database, install the tzdata package (python -m pip install tzdata); zoneinfo uses it automatically.

astimezone() converts an aware datetime to another zone. The instant stays the same; only the wall-clock reading changes:

python
order_placed = datetime(2026, 3, 1, 9, 30, tzinfo=IST)
print(order_placed.astimezone(timezone.utc))
print(order_placed.astimezone(ZoneInfo("America/New_York")))
print(datetime.now(timezone.utc).tzinfo)
text
2026-03-01 04:00:00+00:00
2026-02-28 23:00:00-05:00
UTC

Note the date changed in New York: the order was placed on 1 March in India and on 28 February in New York. A daily sales report grouped by naive dates would disagree between the two offices.

For the current time, use datetime.now(timezone.utc) or datetime.now(IST). The older datetime.utcnow() returns a naive value and is deprecated from Python 3.12.

The storage rule: store and compute in UTC, convert to a local zone only for display. India Standard Time has no daylight saving, so IST is always UTC+05:30, but your users, servers and data sources may not all be in India.

Parsing strings into datetimes

Input usually arrives as text. Two tools cover almost everything:

python
print(datetime.fromisoformat("2026-03-01T09:30:00+05:30"))
print(datetime.fromisoformat("2026-03-01T04:00:00Z"))

legacy = datetime.strptime("01/03/2026 09:30", "%d/%m/%Y %H:%M")
print(legacy, legacy.tzinfo)
print(legacy.replace(tzinfo=IST).isoformat())
text
2026-03-01 09:30:00+05:30
2026-03-01 04:00:00+00:00
2026-03-01 09:30:00 None
2026-03-01T09:30:00+05:30
  • fromisoformat() reads ISO 8601 strings, the format APIs and databases usually produce. Since Python 3.11 it accepts most ISO forms, including a trailing Z for UTC; earlier versions were much stricter.
  • strptime() reads any other layout from a format string. The result is naive unless the string itself contains an offset (%z), so attach the zone you know the data was recorded in with replace(tzinfo=...).

The trap in the middle example is 01/03/2026: is that 1 March or 3 January? strptime does exactly what the format says, so the format must come from knowing the source, not from guessing.

Formatting for people

strftime() goes the other way, and isoformat() produces the machine-readable form:

python
print(order_placed.strftime("%d %b %Y, %I:%M %p"))
print(order_placed.strftime("%A"), order_placed.isoformat())
text
01 Mar 2026, 09:30 AM
Sunday 2026-03-01T09:30:00+05:30

Use isoformat() whenever the value is going into a file, a JSON payload or an API; see working with JSON in Python for serialising datetimes. Month and day names from %b and %A follow the current locale.

Arithmetic: when one day is not 24 hours

A timedelta is a duration. Subtracting dates gives one, and adding one moves a date:

python
print(date(2026, 12, 31) - date(2026, 9, 25))
print(date(2026, 1, 31) + timedelta(days=30))
text
97 days, 0:00:00
2026-03-02

There is no "add one month" in timedelta, because months differ in length; the second line shows the effect of adding 30 days instead. For calendar months, compute the target year and month yourself, or use a library built for it.

The subtle case is daylight saving. For aware datetimes in the same zone, Python does wall-clock arithmetic (PEP 495): adding one day keeps the same clock time on the next day, even when that day is shorter. In New York, clocks jumped forward on 8 March 2026:

python
NY = ZoneInfo("America/New_York")
before = datetime(2026, 3, 7, 12, 0, tzinfo=NY)
after = before + timedelta(days=1)
print(after)
print(after.astimezone(timezone.utc) - before.astimezone(timezone.utc))
text
2026-03-08 12:00:00-04:00
23:00:00

Noon to noon, but only 23 hours actually passed. If you are measuring elapsed time, such as a job's runtime or a billing period in hours, convert both ends to UTC first, as in the last line. If you are scheduling something for "the same time tomorrow", wall-clock arithmetic is what you want. Clocks going back create the opposite problem: one wall-clock hour happens twice, and the fold attribute says which of the two a datetime means.

A short checklist

  1. Attach a zone at the edge of your program: when parsing input, reading a database value or calling now().
  2. Store and compare in UTC; convert to local time only for display.
  3. Use fromisoformat() for ISO strings, and an explicit strptime() format for everything else.
  4. Write timestamps out with isoformat(), so the offset travels with the value.
  5. Measure durations in UTC; schedule by the local clock.

How do I get a Unix timestamp, and turn one back into a datetime?

For an aware datetime, dt.timestamp() returns seconds since 1970-01-01 UTC. To go back, use datetime.fromtimestamp(ts, tz=timezone.utc); without the tz argument you get a naive datetime in the computer's local time, which is rarely what you want on a server.

How do I compare only the dates and ignore the time?

Convert both values to the same zone first, then compare their .date() results. Skipping the conversion compares calendar dates in two different places, which is the bug from the New York example above.

Should I use pytz?

Not in new code on Python 3.9 or later. zoneinfo is in the standard library and works with normal datetime arithmetic, whereas pytz needs its own localize() method to attach a zone correctly.

In tables of data, pandas applies the same ideas at scale with to_datetime, tz_localize and tz_convert; see the pandas guide. Timestamps inside CSV files are covered in reading and writing CSV files in Python. The standard library modules used here, datetime and zoneinfo among them, are part of the modules and packages section of the Python Programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Python?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course