Python datetime — utcnow() DST Bug That Shifts Dates
datetime.
- 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
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.
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.
datetime.now() — naive, but fine for UIdatetime.fromisoformat() — faster and handles TZtimedelta — 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.
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.
- 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
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.
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.
timestamp() returns a float; microseconds beyond 6 decimal places are truncated.The Naive Datetime That Cost a Company $40k in Late Fees
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.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.- 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.
datetime() or strptime. Consider using dateutil.parser for robust parsing.astimezone(). Make sure to .replace(tzinfo=...) or use .astimezone() on an aware object.Key takeaways
Common mistakes to avoid
4 patternsUsing datetime.now() or datetime.utcnow() in production
Using replace(tzinfo=...) instead of astimezone()
replace() for correcting a wrong timezone label on an already correct instant.Adding months via timedelta
Ignoring parser exceptions
Interview Questions on This Topic
What is the difference between a naive and an aware datetime in Python?
Frequently Asked Questions
That's Python Libraries. Mark it forged?
3 min read · try the examples if you haven't