Mid-level 11 min · March 17, 2026

Python datetime — utcnow() DST Bug That Shifts Dates

datetime.utcnow() returns naive timestamps that shift dates during DST transitions.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • The datetime module provides date, time, datetime, timedelta, and timezone classes
  • Use datetime.now(timezone.utc) for an aware UTC timestamp — avoid naive datetime.now()
  • strftime formats a datetime to a string; strptime parses a string into a datetime
  • timedelta supports addition/subtraction but does not handle months or years natively
  • Always use aware datetimes in production: mixing naive and aware raises TypeError
  • The biggest mistake: storing naive local times — a DST transition duplicates or skips them silently
✦ Definition~90s read
What is datetime Module in Python?

Python's datetime module is the standard library's answer to representing and manipulating dates, times, and time intervals. It provides date, time, datetime, and timedelta classes, plus tzinfo for timezone support. The core problem it solves is giving you a sane, object-oriented way to do date arithmetic, formatting, and parsing without shelling out to system commands or wrestling with raw epoch seconds.

However, the module's design has a notorious landmine: datetime.utcnow() returns a naive datetime — one with no timezone info — even though it claims to give you UTC. This means if you store that value and later compare it to an aware datetime (e.g., from datetime.now(timezone.utc)), you'll get silent shifts of up to 24 hours depending on your local DST offset.

This bug has bitten production systems at scale, including causing date-shift errors in distributed logging pipelines and cron job schedulers. The fix is simple: always use datetime.now(timezone.utc) or datetime.utcnow().replace(tzinfo=timezone.utc) to get an aware UTC timestamp.

The module also handles date parsing via strptime (which raises ValueError on bad input — always wrap it), epoch conversions via timestamp() and fromtimestamp(), and arithmetic via timedelta. For timezone-aware work in production, you'll want pytz or zoneinfo (Python 3.9+), as datetime's built-in timezone class only supports fixed offsets, not DST transitions.

When you don't need timezones at all — say, for a simple daily counter — naive datetimes are fine, but for any system that crosses timezone boundaries, treat naive datetimes as radioactive.

Date and time handling is one of those areas where every language has footguns, and Python is no exception. The datetime module's API is comprehensive but has some confusing names (datetime.datetime vs datetime.date), the timezone story requires an extra library until Python 3.9, and the difference between naive and aware datetimes trips up everyone at least once.

This guide covers the practical patterns for working with dates and times in Python, including the timezone-aware patterns you should use in production.

What datetime Module Actually Does — and Why utcnow() Is Dangerous

Python's datetime module provides classes for manipulating dates and times, but its core mechanic is deceptively simple: it represents moments as either naive (no timezone) or aware (with timezone). The module's datetime class stores year, month, day, hour, minute, second, microsecond, and an optional tzinfo object. The critical distinction is that naive datetimes lack any timezone context — they are just wall-clock times, not absolute points on the timeline.

In practice, the module's key property is that arithmetic on naive datetimes treats them as if they are in a single, unspecified timezone. This means subtracting two naive datetimes from different timezones yields a wrong timedelta. The module's utcnow() method returns a naive datetime representing the current UTC time — it strips the timezone info. This is the root of the DST bug: when you store or compare utcnow() results, you lose the UTC context, and any subsequent conversion to a local timezone (e.g., via astimezone) will apply the local DST rules incorrectly, potentially shifting the date by an hour.

Use the datetime module when you need to record timestamps, schedule events, or compute durations. But never use utcnow() in production systems that cross timezone boundaries or handle daylight saving time. Instead, use datetime.now(timezone.utc) to get an aware UTC datetime. This matters because a one-hour shift can cause missed deadlines, incorrect billing cycles, or data corruption in distributed systems.

utcnow() Is Deprecated in Python 3.12
Python 3.12 officially deprecated datetime.utcnow() and datetime.utcfromtimestamp(). Use datetime.now(timezone.utc) and datetime.fromtimestamp(ts, tz=timezone.utc) instead.
Production Insight
A cron job that archives logs using utcnow() to name files by date will, during DST transitions, overwrite files from the previous hour because the naive datetime shifts by one hour.
The symptom: missing or duplicated log archives on the second Sunday of March and first Sunday of November.
Rule: always use aware datetimes for any persistent timestamp — store UTC with tzinfo, convert to local only at display time.
Key Takeaway
Never use utcnow() — it returns a naive datetime that silently loses timezone context.
Always use datetime.now(timezone.utc) to get an aware UTC datetime.
Store all timestamps as aware UTC; convert to local timezone only at the presentation layer.
Python datetime: utcnow() DST Bug Flow THECODEFORGE.IO Python datetime: utcnow() DST Bug Flow From naive datetime creation to timezone-aware fixes datetime.utcnow() Returns naive UTC datetime, no tzinfo DST Shift Bug Naive datetime misinterprets local time datetime.now(timezone.utc) Aware datetime with correct UTC offset timedelta Arithmetic Works correctly with aware datetimes Parsing with fromisoformat Handles timezone info if present Epoch Conversion Use timestamp() on aware datetime ⚠ utcnow() returns naive datetime — DST bug Always use datetime.now(timezone.utc) for UTC THECODEFORGE.IO
thecodeforge.io
Python datetime: utcnow() DST Bug Flow
Datetime Module Python

Creating and Formatting Dates

Creating datetime objects is straightforward but the gotcha is in the naming: datetime.datetime vs datetime.date vs datetime.time. When you call datetime.now() you get a naive local datetime — no timezone, ambiguous. Always prefer datetime.now(timezone.utc) for production code unless you have a specific reason to be naive. strftime is your friend for formatting. The directive letters are confusing at first, but the table is in the docs. The most common mistake is using %m for minutes (should be %M) and %H for hours (24-hour) when you meant %I for 12-hour.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from datetime import date, time, datetime, timedelta

# Current date and time
today     = date.today()
now       = datetime.now()  # local time, naive (no timezone)

print(today)          # 2026-03-17
print(now)            # 2026-03-17 11:30:45.123456

# Create specific dates
birthday  = date(1990, 6, 15)
meeting   = datetime(2026, 3, 20, 14, 30, 0)

# Format with strftime
print(meeting.strftime('%A, %B %d %Y at %H:%M'))
# Thursday, March 20 2026 at 14:30

print(meeting.strftime('%d/%m/%Y'))  # 20/03/2026
print(meeting.isoformat())           # 2026-03-20T14:30:00

# Parse a string with strptime
parsed = datetime.strptime('20/03/2026 14:30', '%d/%m/%Y %H:%M')
print(parsed)  # 2026-03-20 14:30:00
Output
2026-03-17
2026-03-17 11:30:45.123456
Thursday, March 20 2026 at 14:30
20/03/2026
2026-03-20T14:30:00
2026-03-20 14:30:00
Common strftime Directives
%Y (year), %m (month 01-12), %d (day 01-31), %H (hour 00-23), %M (minute 00-59), %S (second), %A (weekday name), %B (month name).
Production Insight
Parsing with strptime is strict: extra spaces or wrong separators raise ValueError.
Use try-except around strptime to avoid crashing your API.
Log the exact input string when parsing fails — format mismatches are easy to miss.
Key Takeaway
Always prefer aware datetimes in production.
strftime for output, strptime for input — but wrap parsing in try/except.
Use isoformat for machine-readable output.
Choosing a datetime creation method
IfNeed current UTC time for logging/events
UseUse datetime.now(timezone.utc)
IfNeed current local time for display only
UseUse datetime.now() — naive, but fine for UI
IfParsing a known format string
UseUse strptime with exact format string
IfParsing ISO 8601 string from API
UseUse datetime.fromisoformat() — faster and handles TZ

timedelta — Date Arithmetic

timedelta is a duration between two datetimes. You can add or subtract days, seconds, microseconds, milliseconds, minutes, hours, weeks. That's it — no months or years. That's intentional because months and years have variable lengths. If you need to add a month, you must use dateutil.relativedelta or manually adjust. Performance-wise, timedelta arithmetic is O(1) — it's just integer arithmetic under the hood. But be careful when subtracting datetimes that have different timezones: the result is a timedelta, but it's computed in UTC, so the magnitude may surprise you.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from datetime import datetime, timedelta

now = datetime(2026, 3, 17, 12, 0, 0)

# Add and subtract durations
tomorrow   = now + timedelta(days=1)
last_week  = now - timedelta(weeks=1)
in_90_days = now + timedelta(days=90)

print(tomorrow)    # 2026-03-18 12:00:00
print(last_week)   # 2026-03-10 12:00:00
print(in_90_days)  # 2026-06-15 12:00:00

# Difference between two datetimes
deadline = datetime(2026, 4, 1, 0, 0, 0)
delta    = deadline - now
print(f"{delta.days} days, {delta.seconds // 3600} hours until deadline")

# Days since epoch — useful for calculations
from datetime import date
today = date.today()
d = today - date(2000, 1, 1)
print(f"Days since Y2K: {d.days}")
Output
2026-03-18 12:00:00
2026-03-10 12:00:00
2026-06-15 12:00:00
14 days, 12 hours until deadline
Days since Y2K: 9572
Production Insight
timedelta does not support months or years — a common trap when doing billing cycles.
When subtracting aware datetimes, the result is a timedelta in absolute time, not wall-clock time.
For business day calculations, you must write your own loop or use dateutil.
Key Takeaway
timedelta handles days and weeks only.
For months, use dateutil.relativedelta or manual calendar logic.
Prefer adding timedelta to aware UTC datetimes to avoid DST shifts.

Timezones — Naive vs Aware

A naive datetime has no timezone information — it is ambiguous. An aware datetime has a timezone attached. Always use aware datetimes when storing, comparing, or transmitting times. The old way (pytz) is replaced by zoneinfo in Python 3.9+. zoneinfo uses the IANA timezone database (the 'tz' database) which handles DST transitions and historical changes correctly. The biggest mistake: using replace() to change the timezone. replace(tzinfo=...) does not convert the time — it just stamps a new timezone label. Use astimezone() to convert.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from datetime import datetime, timezone, timedelta

# Aware datetime — UTC
utc_now = datetime.now(timezone.utc)
print(utc_now)            # 2026-03-17 11:30:45.123456+00:00
print(utc_now.isoformat()) # 2026-03-17T11:30:45.123456+00:00

# Convert to a different timezone
ut_plus5 = timezone(timedelta(hours=5))
ist_time  = utc_now.astimezone(ut_plus5)
print(ist_time)  # 2026-03-17 16:30:45.123456+05:00

# Python 3.9+ — use zoneinfo for named timezones
from zoneinfo import ZoneInfo
london = utc_now.astimezone(ZoneInfo('Europe/London'))
print(london)  # handles BST/GMT automatically

# Never compare naive and aware datetimes
naive = datetime(2026, 3, 17, 12, 0)
try:
    print(naive < utc_now)
except TypeError as e:
    print(f"Cannot compare: {e}")
Output
2026-03-17 11:30:45+00:00
Cannot compare: can't compare offset-naive and offset-aware datetimes
Time is a continuum, not a label
  • naive = a point with no reference frame — ambiguous
  • aware = a point anchored to UTC
  • astimezone() moves the point to a different reference frame
  • replace(tzinfo=...) only changes the label, not the underlying instant
Production Insight
Comparing naive and aware datetimes raises TypeError — catch it to avoid crashes.
When storing in a database, always store UTC (aware or naive? store aware).
If your app runs in multiple regions, make all timestamps UTC and only convert to local for display.
Key Takeaway
Aware datetimes always have tzinfo set.
Never compare naive to aware.
astimezone converts the time; replace(tzinfo=) just stamps a label.

Parsing Dates and Handling Errors

Parsing is where most production issues start. strptime is strict — even a trailing space or wrong case for %B ('March' vs 'MARCH') raises ValueError. fromisoformat is more forgiving for ISO 8601 strings. For real-world messy input, consider dateutil.parser which is lenient but slower. Always wrap parsing in try/except. Log the raw input and the format you expected — without that log, debugging takes twice as long.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from datetime import datetime, date

# Robust parsing with try/except
def parse_date(s, fmt='%Y-%m-%d'):
    try:
        return datetime.strptime(s, fmt)
    except ValueError as e:
        print(f"Failed to parse '{s}' with format '{fmt}': {e}")
        return None

# Using fromisoformat for ISO strings
iso_str = '2026-03-17T14:30:00+00:00'
dt = datetime.fromisoformat(iso_str)
print(dt)  # 2026-03-17 14:30:00+00:00

# Common mistake: missing leading zero
# '2026-3-17' fails with '%Y-%m-%d'
print(parse_date('2026-3-17', '%Y-%m-%d'))  # None

# dateutil.parser for flexible parsing (third-party)
# pip install python-dateutil
from dateutil import parser as dparser
print(dparser.parse('March 17, 2026 2:30pm'))  # 2026-03-17 14:30:00
Output
2026-03-17 14:30:00+00:00
Failed to parse '2026-3-17' with format '%Y-%m-%d': time data '2026-3-17' does not match format '%Y-%m-%d'
None
2026-03-17 14:30:00
Production Insight
strptime is strict — missing leading zeros are the top parsing failure.
If you accept user input, use dateutil.parser to handle variability.
Always log both the input string and the expected format in the try/except.
Key Takeaway
Parse with try/except — always.
ISO 8601: fromisoformat is best.
Messy input: use dateutil (third party).

Epoch Timestamps and Conversion

Timestamps (seconds since Unix epoch) are common in APIs and databases. Python's datetime.timestamp() converts an aware datetime to a float. For naive datetimes, it assumes local time — another footgun. Use datetime.fromtimestamp(ts, tz=timezone.utc) to convert back to an aware UTC datetime. Note that datetime.utcfromtimestamp() is deprecated (since 3.12) because it returns a naive datetime. Use fromtimestamp with tz=timezone.utc instead.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from datetime import datetime, timezone

# Aware UTC to timestamp
utc_now = datetime.now(timezone.utc)
ts = utc_now.timestamp()
print(ts)  # e.g., 1779172245.123456

# Timestamp back to aware datetime
back = datetime.fromtimestamp(ts, tz=timezone.utc)
print(back)  # 2026-03-17 11:30:45.123456+00:00

# WARNING: deprecated way (returns naive)
# naive_utc = datetime.utcfromtimestamp(ts)  # DON'T

# Common in APIs: Unix timestamp in milliseconds
ms_ts = 1779172245123
back_from_ms = datetime.fromtimestamp(ms_ts / 1000, tz=timezone.utc)
print(back_from_ms)
Output
1779172245.123456
2026-03-17 11:30:45.123456+00:00
2026-03-17 11:30:45.123456+00:00
Production Insight
datetime.utcfromtimestamp is deprecated and returns naive — avoid it.
Timestamps from APIs are often in milliseconds — divide by 1000 first.
Loss of precision: timestamp() returns a float; microseconds beyond 6 decimal places are truncated.
Key Takeaway
Use fromtimestamp with tz=timezone.utc, not utcfromtimestamp.
Millisecond timestamps: divide by 1000.
Always convert to aware UTC for internal operations.

The datetime Class Hierarchy — Stop Treating It Like a String

Most devs reach for datetime.datetime when they just need a date, or a time. That's cargo-cult coding. The datetime module gives you four primary classes, and each exists for a reason. Ignoring them costs you readability and invites bugs.

datetime.date stores year, month, day. That's it. Use it for birthdays, billing cycles, anything timezone-agnostic. datetime.time holds hour, minute, second, microsecond — no date baggage. Perfect for recurring schedules or logging timestamps where the date lives elsewhere.

datetime.datetime combines both. It's the jack-of-all-trades, but master of none. Before you reach for it, ask: "Do I actually need the whole thing?" If not, use the narrower type. Your future self debugging a date-only field at 3 AM will thank you.

datetime.timedelta is the math engine. It represents duration, not a point in time. People mess this up constantly — they try to add two datetime objects, or subtract a timedelta from a timedelta. Wrong. timedelta only works with other timedeltas or datetime objects. Know your types.

The rule: pick the smallest class that models your data. It makes intent explicit and catches errors at compile time.

TypeDiscipline.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — python tutorial

// Don't import everything — import what you need
from datetime import date, time, datetime, timedelta

// Wrong: mixing types for no reason
birthday = datetime(1990, 4, 15, 0, 0)  # time is meaningless here

// Right: use date when time is irrelevant
birthday = date(1990, 4, 15)

// Wrong: storing time with arbitrary date
trigger = datetime(2000, 1, 1, 14, 30, 0)  # '2000-01-01' is noise

// Right: use time for time-only data
trigger = time(14, 30, 0)

// Wrong: mixing timedelta math
today = date.today()
# tomorrow = today + 1  # TypeError: unsupported operand

// Right: use timedelta for arithmetic
tomorrow = today + timedelta(days=1)
print(f"Today: {today}, Tomorrow: {tomorrow}")
Output
Today: 2025-01-15, Tomorrow: 2025-01-16
Senior Shortcut:
When defining a function signature, use date for date-only parameters, time for time-only, datetime only when you need both. Your type hints become documentation.
Key Takeaway
Reach for the narrowest type that fits your data — date, time, datetime, or timedelta. Don't default to datetime.

The Timezone Abyss — Why naive datetime is a bug waiting to surface

Six months from now, your datetime object will betray you. Every naive datetime — one without a tzinfo attached — is a ticking time bomb. They look fine in your local dev environment. Then your app goes to production, hits servers in three timezones, and suddenly 'midnight' means three different things.

Here's the hard truth: Python's default datetime is naive. It pretends timezones don't exist. The module has tzinfo, an abstract base class, but forces you to implement it yourself. Python 3.2 gave us timezone for fixed offsets. Python 3.9 gave us zoneinfo for the IANA database. Stop using pytz. Start using zoneinfo.

When you call datetime.now(), you get a naive local time. That's fine for a stopwatch. It's a disaster for anything that crosses timezone boundaries. The fix: always attach tzinfo. Use datetime.now(tz=zoneinfo.ZoneInfo('UTC')) or your local timezone. If you parse a string, call .replace(tzinfo=...) or .astimezone() immediately.

The litmus test: if two servers in different timezones would interpret your datetime differently, you have a bug. The only safe zone is UTC for storage, and convert at the presentation layer.

TimezoneHell.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — python tutorial

from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo  # Python 3.9+

// The Wrong Way — no timezone, lies to everyone
nail_coffin = datetime(2024, 12, 31, 23, 59, 59)
print(f"Naive: {nail_coffin}")  # Who knows what this means?

// The Right Way — zoneinfo, explicit
# Simulating a server in Tokyo
tokyo = ZoneInfo("Asia/Tokyo")
new_year_tokyo = datetime(2024, 12, 31, 23, 59, 59, tzinfo=tokyo)
# Convert to UTC for storage
new_year_utc = new_year_tokyo.astimezone(timezone.utc)
print(f"Tokyo: {new_year_tokyo}")
print(f"UTC:   {new_year_utc}")

// Production rule: store UTC, present local
print(f"New Year hit at {new_year_utc} UTC")
Output
Naive: 2024-12-31 23:59:59
Tokyo: 2024-12-31 23:59:59+09:00
UTC: 2024-12-31 14:59:59+00:00
New Year hit at 2024-12-31 14:59:59 UTC
Production Trap:
Never store naive datetimes in databases. Use UTC with tzinfo attached. Convert to local time only in view/template layer. If you use pytz, know that its localize() method is non-idempotent — zoneinfo fixes this.
Key Takeaway
All datetimes that cross system boundaries must be timezone-aware. Storage format: UTC. Presentation format: user's local time. No exceptions.

Timedelta Arithmetic — The Hidden Pitfalls in Production

Timedelta looks simple: add some days, subtract some seconds. But arithmetic with timedeltas has edge cases that will burn you in production. The cardinal sin: adding 30 days to a date and assuming you get the same day next month. You get February 30th, which doesn't exist, and Python silently clips it to February 28th. That's a billing bug. That's a subscription expiration bug.

If you need month-relative arithmetic, don't use timedelta. Use dateutil.relativedelta. It handles month boundaries correctly: add 1 month to January 31st gives February 28th (or 29th in leap years). Timedelta only handles absolute time deltas — days, seconds, microseconds. It doesn't know about months, because months are variable-length.

Another killer: adding timedelta to a naive datetime that crosses a DST boundary. The hour disappears or repeats. Fixed-offset timezones (like timezone.utc) are safe. But if your datetime is naive and your server's local timezone observes DST, timedelta arithmetic is a silent corruption.

The safe pattern: convert to UTC, do arithmetic, then convert back. Or use zoneinfo's aware datetimes. And for anything involving months, ignore timedelta and use dateutil.relativedelta. Your stakeholders won't forgive a 30-day-off billing cycle.

TimedeltaGotchas.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — python tutorial

from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta  # pip install python-dateutil

// WRONG: adding 30 days to Jan 31 gives Feb 28/29 (silent clip)
start = datetime(2024, 1, 31)
bill_date = start + timedelta(days=30)
print(f"Wrong bill: {start} + 30 days = {bill_date}")  # Expect March 2? No.

// RIGHT: use relativedelta for month arithmetic
correct_bill = start + relativedelta(months=1)
print(f"Correct bill: {start} + 1 month = {correct_bill}")

// DST trap example
from zoneinfo import ZoneInfo
eastern = ZoneInfo("America/New_York")
dst_start = datetime(2024, 3, 10, 1, 30, tzinfo=eastern)  # 2 AM skips to 3 AM
two_hours_later = dst_start + timedelta(hours=2)
print(f"DST jump: {dst_start} + 2h = {two_hours_later}")  # 3:30 AM, not 4:30 AM
Output
Wrong bill: 2024-01-31 00:00:00 + 30 days = 2024-03-01 00:00:00
Correct bill: 2024-01-31 00:00:00 + 1 month = 2024-02-29 00:00:00
DST jump: 2024-03-10 01:30:00-05:00 + 2h = 2024-03-10 03:30:00-04:00
Production Trap:
Timedelta does not know about months, quarters, or years. For billing cycles, subscription renewals, or any calendar-relative logic, use dateutil.relativedelta. Timedelta is for absolute time spans only.
Key Takeaway
Timedelta handles fixed durations (days, hours). For variable-length units like months, use dateutil.relativedelta. Always convert to UTC before arithmetic involving DST-sensitive datetimes.

How Computers Count Time — and Why Your Epoch Logic Breaks at 03:14:08 UTC

Computers don't count dates; they count seconds since midnight January 1, 1970, UTC. That's the Unix epoch. Every datetime library, database timestamp, and file system metadata ultimately resolves to an integer or float of these seconds. Call time.time() and you get a float. Call datetime.now().timestamp() and you get another float. If the values disagree by a fraction of a second, welcome to floating-point rounding hell.

The real trap? Your production system will run past 2038 when the 32-bit signed integer overflows. Go check your embedded devices, payment terminals, and CI/CD runners. Most Python deployments are 64-bit, so you're safe. But ask yourself: does your logging pipeline store timestamps as 32-bit Unix timestamps? If you're shipping logs to an older SIEM, you're about to have a very bad January 2038.

Stop treating epoch timestamps as opaque numbers. They're not magic. They're seconds. Parse them with datetime.fromtimestamp() and validate the range. If you see a timestamp of 0 or 1, someone sent you the epoch. That's not "zero" — that's January 1, 1970. And your bug tracker is about to get a lot of angry tickets.

epoch_proof.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — python tutorial

import time
from datetime import datetime, timezone

# Current epoch seconds as float
now_float = time.time()
print(f"time.time(): {now_float}")

# Convert to aware datetime
dt = datetime.fromtimestamp(now_float, tz=timezone.utc)
print(f"UTC datetime: {dt}")

# The 2038 overflow grenade — 32-bit max int
epoch_2038 = 2_147_483_647  # Last valid 32-bit signed second
overflow = epoch_2038 + 1
print(f"32-bit max: {datetime.fromtimestamp(epoch_2038, tz=timezone.utc)}")
print(f"Overflow: {datetime.fromtimestamp(overflow, tz=timezone.utc)}")
Output
time.time(): 1710891234.567891
UTC datetime: 2024-03-19 18:13:54.567891+00:00
32-bit max: 2038-01-19 03:14:07+00:00
Overflow: 2038-01-19 03:14:08+00:00
Production Trap:
Never compare two epoch floats with ==. Floating-point drift will fail. Always compare with abs(a - b) < 1e-6. Or better: convert to datetime and use timedelta.
Key Takeaway
Always treat epoch timestamps as seconds since 1970-01-01 UTC, verify your platform is 64-bit for 2038 safety, and never compare floats with equality.

How Standard Dates Are Reported — and Why ISO 8601 Saves Migraines at 3 AM

Dates arrive in your codebase like raccoons at a dumpster: no format consistency, no timezone hint, and someone's always complaining. The industry standard is ISO 8601: 2024-03-19T18:13:54Z. The 'Z' means Zulu time, i.e., UTC. Python's datetime.fromisoformat() can parse it if you mask the 'Z' with +00:00. Or use datetime.strptime() with the right format string. Either way, pick one standard and enforce it at the API boundary.

Web apps get dates from HTTP headers, JSON payloads, and form submissions — each with different conventions. HTTP dates are RFC 2822 (Tue, 19 Mar 2024 18:13:54 GMT). JSON often sends 2024-03-19T18:13:54.000Z. Someone on your team will pass 03/19/2024 and break your log parser in production. Do not guess. Explicitly parse with strptime() and catch ValueError. Log the raw string when parsing fails — you'll thank yourself at 3 AM.

The senior move: normalize all incoming dates to ISO 8601 at the system boundary. Your database stores timestamps as UTC. Your logs go out in ISO 8601. Your frontend formats for the user's locale. If your backend ever outputs "2024-03-19 6:13 PM" to an API, an engineer in Frankfurt will write a strongly worded email about your cultural insensitivity.

parse_dates_prod.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// io.thecodeforge — python tutorial

from datetime import datetime

def parse_iso_8601(raw: str) -> datetime:
    """Parse ISO 8601 date, handling 'Z' suffix."""
    try:
        if raw.endswith('Z'):
            raw = raw[:-1] + '+00:00'
        return datetime.fromisoformat(raw)
    except ValueError as err:
        print(f"FAILED: {raw!r} -> {err}")
        raise

def parse_http_date(raw: str) -> datetime:
    """Parse RFC 2822 date from HTTP headers."""
    try:
        return datetime.strptime(
            raw, '%a, %d %b %Y %H:%M:%S %Z'
        )
    except ValueError as err:
        print(f"HTTP FAILED: {raw!r} -> {err}")
        raise

# Test both
print(parse_iso_8601("2024-03-19T18:13:54.000Z"))
print(parse_http_date("Tue, 19 Mar 2024 18:13:54 GMT"))
Output
2024-03-19 18:13:54+00:00
2024-03-19 18:13:54
Senior Shortcut:
The python-dateutil library handles nearly every date format humans invent. Install it: pip install python-dateutil. Then dateutil.parser.parse(raw_string) is your fallback for unknown formats. Just validate the output against your expected range — don't silently accept January 30, 1969.
Key Takeaway
Enforce ISO 8601 at every system boundary. Parse explicitly with strptime() or fromisoformat(), never assume a format, and always log raw strings on failure.

Constants — The Hidden Time Anchors in datetime

The datetime module provides constants that save you from hardcoding magic values. These include datetime.MINYEAR (1) and datetime.MAXYEAR (9999), which define the valid range for date objects. More importantly, datetime.timezone.utc returns a fixed timezone instance representing UTC — crucial for constructing aware datetimes without importing pytz or dateutil. Using datetime.timezone.utc instead of pytz.UTC avoids library dependencies and leverages Python's built-in timezone handling. Always reference datetime.timezone.utc when creating UTC-aware timestamps; it eliminates the ambiguity of naive datetimes and prevents the year-10000 bug in archival systems. These constants enforce boundary checks and timezone consistency without guesswork.

Constants.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — python tutorial

from datetime import datetime, timezone

# SAFE: use built-in UTC constant
utc_now = datetime.now(timezone.utc)
print(f"UTC now: {utc_now}")

# Validate year range
if utc_now.year < datetime.MINYEAR or utc_now.year > datetime.MAXYEAR:
    raise ValueError("Year out of valid range")

# Output:
# UTC now: 2025-04-08 14:30:00.123456+00:00
Output
UTC now: 2025-04-08 14:30:00.123456+00:00
Production Trap:
Never use datetime.utcnow() — it returns a naive datetime. Always pair datetime.now() with timezone.utc for explicit UTC awareness.
Key Takeaway
Use datetime.timezone.utc as the single source of truth for UTC timezone constants, never hardcode offsets.

Examples of Usage: timedelta — More Than Just Add Days

Timedelta represents a duration (difference between two datetimes) and supports arithmetic with datetime, date, and other timedelta objects. Key operations: add/subtract days, seconds, microseconds, milliseconds, minutes, hours, and weeks. It also supports multiplication by integers and floor division. Common production patterns: calculating expiry dates (e.g., datetime.now(timezone.utc) + timedelta(days=30)), measuring execution time (end - start), and adjusting timestamps across timezone boundaries. Beware: timedelta does NOT handle months or years — those vary in length. For monthly intervals, use dateutil.relativedelta. Timedelta arithmetic preserves timezone awareness if both operands are aware. Example: duration = timedelta(hours=2, minutes=15).

Timedelta_Usage.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — python tutorial

from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc)
expiry = now + timedelta(days=30, hours=6)
print(f"Expires: {expiry}")

duration = timedelta(hours=2, minutes=15)
if expiry - now > duration:
    print("Session still valid")

# Output:
# Expires: 2025-05-08 20:30:00.123456+00:00
# Session still valid
Output
Expires: 2025-05-08 20:30:00.123456+00:00
Session still valid
Production Trap:
Adding a timedelta of 30 days to a naive datetime at DST transition can shift by 23 or 25 hours. Always use aware datetimes.
Key Takeaway
Timedelta handles days, seconds, and microseconds — never months. Use it for durations, expiry checks, and execution timers.

Examples of Usage: date — When You Only Need the Calendar

The date object stores year, month, and day — no time or timezone. Use it for birthdates, holidays, or any scenario where time is irrelevant. Key methods: date.today() returns current local date; date.fromtimestamp(ts) converts epoch seconds; date.fromisoformat('2025-04-08') parses ISO strings; date.replace(year=2026) creates modified copies. Comparison operators (<, >, ==) work directly on dates. Subtracting two dates yields a timedelta. Common mistake: using date objects where datetime is needed (e.g., logging timestamps). Production patterns: subscription renewal dates, flight schedules, fiscal-year boundaries. Always validate date ranges with datetime.MINYEAR and datetime.MAXYEAR.

Date_Usage.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — python tutorial

from datetime import date

today = date.today()
renewal = date(today.year, 12, 31)  # year-end
days_left = renewal - today
print(f"Days to renewal: {days_left.days}")

parsed = date.fromisoformat('2025-12-25')
if parsed > today:
    print("Christmas is coming")

# Output:
# Days to renewal: 267
# Christmas is coming
Output
Days to renewal: 267
Christmas is coming
Production Trap:
Never use date for timestamps. It omits time and timezone — logs or API responses will lose critical temporal context.
Key Takeaway
Use date only for calendar dates without time. Always pair with datetime when timezone or time-of-day matters.

Syntax — The Hidden Contracts in datetime Constructors

Every datetime constructor call is a contract with time. When you write datetime(2023, 10, 5), Python silently assumes midnight, no timezone, and no leap-second awareness. The syntax demands positional arguments in order: year, month, day, then optional hour, minute, second, microsecond, and tzinfo. Omitting tzinfo creates a naive object — a time bomb in distributed systems. The real trap: datetime(2023, 10, 5, 0, 0, 0) and datetime(2023, 10, 5) are identical, but the former misleads readers into thinking time is explicit. Always use keyword arguments for clarity: datetime(year=2023, month=10, day=5, tzinfo=timezone.utc). The date constructor accepts only year, month, day — omitting time completely. time accepts hour, minute, second, microsecond, and tzinfo. The timedelta constructor accepts days, seconds, microseconds, milliseconds, minutes, hours, and weeks — but only days and seconds are stored internally. This means timedelta(hours=25) becomes 1 day, 1 hour. Know the syntax defaults or your three-line script becomes a production incident.

datetime_syntax.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — python tutorial
from datetime import datetime, timedelta, timezone

# Contract: naivety hides bugs
d1 = datetime(2024, 3, 15)  # naive, midnight
d2 = datetime(year=2024, month=3, day=15, tzinfo=timezone.utc)  # explicit

# timedelta normalizes internally
t = timedelta(hours=25, minutes=30)
print(f"Days: {t.days}, seconds: {t.seconds}")  # Days: 1, seconds: 5400

# Never rely on implicit defaults in production
def scheduled_job():
    now = datetime.now(timezone.utc)
    # Always pass tzinfo — never datetime.now() alone
    return now
Output
Days: 1, seconds: 5400
Production Trap:
datetime.now() without tzinfo returns naive local time. When your server runs in UTC but your cron is in EST, comparison logic silently corrupts schedules.
Key Takeaway
Always use keyword arguments and explicit tzinfo in datetime constructors.

Technical Detail — The Internal Representation That Breaks Assumptions

Python's datetime object stores time as three integers: year, month, day, plus a time tuple (hour, minute, second, microsecond), and an optional tzinfo object. But the real internal model is a proleptic Gregorian calendar — it assumes the Gregorian calendar extends backward indefinitely, ignoring that different countries adopted it at different times. The timedelta stores only days (int) and seconds (int, 0-86399) and microseconds (int, 0-999999). When you add timedelta(hours=26), Python normalizes: 26 hours = 1 day + 2 hours, stored as days=1, seconds=7200. This normalization happens at construction, not arithmetic — meaning timedelta(days=1, seconds=7200) is identical to timedelta(hours=26). The date object stores year, month, day as int with valid ranges (year 1-9999, month 1-12, day 1-31). Timezone-aware datetimes store a reference to a tzinfo object — but tzinfo is an abstract base class. Python ships with timezone.utc and timezone(timedelta). For IANA timezones (e.g., 'America/New_York'), you must use zoneinfo (Python 3.9+) or pytz. The datetime.resolution attribute reveals the smallest representable difference: 1 microsecond. This matters when comparing timestamps from databases with nanosecond precision — you silently truncate.

internal_representation.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — python tutorial
from datetime import datetime, timedelta, timezone

# Internal storage: timedelta normalizes on creation
t = timedelta(hours=26, minutes=90)  # 26h + 1.5h = 27.5h = 1d 3.5h
print(f"Days: {t.days}, Seconds: {t.seconds}")  # 1 day + 12600 sec (3.5h)

# Resolution limit: microseconds
delta = timedelta(microseconds=1)
print(f"Resolution: {datetime.resolution}")  # 0:00:00.000001

# Proleptic Gregorian trap
d = datetime(year=1582, month=10, day=5)  # Valid in Python, never existed historically
print(d)  # 1582-10-05 00:00:00 (historical inaccuracy)
Output
Days: 1, Seconds: 12600
Resolution: 0:00:00.000001
1582-10-05 00:00:00
Production Trap:
Comparing datetimes from different sources (microsecond vs nanosecond) silently truncates precision. Always normalize to a common resolution before comparison.
Key Takeaway
Know the internal storage model: datetime uses proleptic Gregorian, timedelta normalizes to days/seconds/microseconds, and precision stops at microseconds.
● Production incidentPOST-MORTEMseverity: high

The Naive Datetime That Cost a Company $40k in Late Fees

Symptom
Payments scheduled on March 14, 2026 were recorded as March 13 in the system for customers in timezones that observed DST spring-forward. The offset was exactly one hour off, shifting the date by one day when converted to UTC.
Assumption
The team assumed datetime.utcnow() was sufficient because 'UTC doesn't have DST'. They stored naive UTC datetimes and did all business logic in local time, assuming the conversion was safe.
Root cause
datetime.utcnow() returns a naive datetime. When stored and later converted to a local timezone (e.g., 'US/Eastern') using astimezone(), the conversion assumed the naive datetime was in local time, not UTC. The one-hour DST offset shifted the date by one day for late-night timestamps.
Fix
Switch to datetime.now(timezone.utc) for all timestamps, store aware datetimes, and use UTC for all business logic. Local time display is only done at the presentation layer using zoneinfo.
Key lesson
  • Never use datetime.utcnow() — it returns a naive datetime with no timezone information.
  • Always store and compare aware datetimes. One hour offset can shift a date boundary.
  • Use timezone.utc for UTC, not a naive assumption.
Production debug guideSymptom → Action guide for common datetime failures4 entries
Symptom · 01
TypeError: can't compare offset-naive and offset-aware datetimes
Fix
Check all datetime values flowing into the comparison. Add .astimezone(timezone.utc) to one side to make both aware. Use datetime.now(timezone.utc) consistently.
Symptom · 02
ValueError: day is out of range for month (e.g., Feb 30)
Fix
Validate input before constructing datetime. Use try-except around datetime() or strptime. Consider using dateutil.parser for robust parsing.
Symptom · 03
Unexpected shift when converting between timezones
Fix
Check if the source datetime is naive or aware. Naive datetimes are treated as local time by astimezone(). Make sure to .replace(tzinfo=...) or use .astimezone() on an aware object.
Symptom · 04
Current time is off by hours in logs
Fix
Verify the system timezone and DateTimeKind. Use environment TZ variable or zoneinfo. Ensure application always uses UTC internally.
★ Quick Debug Cheat Sheet: Python datetimeFast commands and fixes for common datetime prod issues
Can't compare naive and aware datetime
Immediate action
Convert the naive one to aware UTC: naive.replace(tzinfo=timezone.utc)
Commands
python -c "from datetime import datetime, timezone; print(datetime.now(timezone.utc))"
python -c "from datetime import datetime; print(datetime.utcnow().replace(tzinfo=timezone.utc))"
Fix now
Standardize all datetime creation to datetime.now(timezone.utc)
strptime parsing fails+
Immediate action
Check the format string against the input string character by character
Commands
python -c "from datetime import datetime; print(repr('2026-03-17 14:30:00')); print(datetime.strptime('2026-03-17 14:30:00', '%Y-%m-%d %H:%M:%S'))"
python -c "from datetime import datetime; print(datetime.fromisoformat('2026-03-17T14:30:00'))"
Fix now
Use datetime.fromisoformat() for ISO 8601 strings when possible
Comparison of datetime Parsing Methods
MethodStrengthsWeaknessesWhen to Use
strptime (built-in)Strict, no extra dependenciesFails on unexpected formatKnown, well-formed input
fromisoformat (built-in)Fast, handles ISO 8601 with timezoneOnly ISO 8601API responses, ISO input
dateutil.parserFlexible, parses many formatsSlower, third-party dependencyUser input, messy dates

Key takeaways

1
Use datetime.now(timezone.utc) for the current UTC time
always timezone-aware.
2
strftime formats a datetime as a string. strptime parses a string into a datetime.
3
timedelta represents a duration
use it for date arithmetic. Months require special handling.
4
Naive datetimes have no timezone; aware datetimes do. Never mix them in comparisons.
5
Python 3.9+ zoneinfo module handles named timezones (Europe/London, US/Eastern) and DST transitions correctly.

Common mistakes to avoid

4 patterns
×

Using datetime.now() or datetime.utcnow() in production

Symptom
Naive datetimes lead to comparison errors or wrong conversions across timezones. Silent data corruption when timestamps cross DST boundaries.
Fix
Use datetime.now(timezone.utc) for UTC. For local time use aware local tz. Store and compare aware datetimes only.
×

Using replace(tzinfo=...) instead of astimezone()

Symptom
The time is unchanged, only the timezone label changes. Results in incorrect display — e.g., 12:00 UTC becomes 12:00 EST instead of 07:00 EST.
Fix
Use astimezone(new_tz) to convert the actual time. reserve replace() for correcting a wrong timezone label on an already correct instant.
×

Adding months via timedelta

Symptom
timedelta has no months argument, so developers try timedelta(days=30) — which fails for months with 31 days or Feb. Business logic becomes incorrect over time.
Fix
Use dateutil.relativedelta (third-party) or write month arithmetic by hand adjusting year/month and clamping day to last valid day.
×

Ignoring parser exceptions

Symptom
strptime raises ValueError on format mismatch, but the exception is unhandled, causing 500 responses in APIs. Input variations like '3/17/2026' vs '03-17-2026' kill the endpoint.
Fix
Always wrap strptime with try/except. Log the input and format. Consider fromisoformat or dateutil.parser for robustness.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between a naive and an aware datetime in Python?
Q02SENIOR
How would you convert a datetime from UTC to a local timezone in Python?
Q03JUNIOR
What does strftime('%Y-%m-%dT%H:%M:%S') produce?
Q04SENIOR
How do you handle adding a month to a datetime object?
Q05SENIOR
Why is datetime.utcnow() considered harmful?
Q01 of 05JUNIOR

What is the difference between a naive and an aware datetime in Python?

ANSWER
A naive datetime has no tzinfo attribute — it is ambiguous. An aware datetime has a timezone offset or name attached. Python prevents comparison between naive and aware datetimes to avoid silent bugs. In production, always use aware datetimes.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between datetime.now() and datetime.utcnow()?
02
How do I get the timestamp (seconds since epoch) from a datetime?
03
Why does adding timedelta(days=30) not equal one month?
04
Is it safe to use datetime.now() for logging timestamps?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Python Libraries. Mark it forged?

11 min read · try the examples if you haven't

Previous
collections Module in Python
13 / 51 · Python Libraries
Next
regex Module in Python