Intermediate Python 📂 Modules · 3 of 6 38 min read

Python Date & Time — datetime, Epoch Timestamps & Format Conversions

Master Python date and time handling with a visual approach. Learn the datetime, time, and epoch models — with a labeled Unix epoch timeline showing where "today" sits and the Y2038 problem, plus a conversion triangle diagram mapping every arrow between human strings, datetime objects, and Unix timestamps. Includes strftime/strptime format codes, timezone-aware datetimes, timedelta arithmetic, and five practical scripts.

Section 01

The Story That Explains Time in Python

The Calendar, The Stopwatch, and The Ticker Tape
Every workshop has three ways of thinking about time. There's the calendar on the wall — "Meeting on Tuesday, July 21st at 3pm." That's how humans see time: years, months, days, hours. There's the stopwatch — "That process took 47.3 seconds." That's how you measure duration. And there's the ticker tape on the machine — "1,784,246,400." A single number that grows by one every second. That's how computers store time internally.

All three describe the same instant. All three need to convert into each other. The calendar becomes the ticker tape when you save data to disk. The ticker tape becomes the calendar when you show it to a user. The stopwatch tells you the difference between two ticker-tape numbers.

Python gives you tools for all three: datetime for the calendar, time for the stopwatch, and Unix timestamps for the ticker tape. Master the conversions between them and you'll never fear a date bug again.

This tutorial focuses on the two conversions you'll do most: turning a human-readable string into a Unix timestamp (an integer), and turning that integer back into any format you want to display. These are the operations behind every log parser, database write, API response, and scheduled job in the world.

💡
The Core Insight

A moment in time and its representation are two different things. "2026-07-12 14:30:00" is a string. 1752341400 is a number. A datetime object is a structured type. They can all point to the same instant — you just need to know which converter to reach for.


Section 02

The Time Landscape — Python's Three Modules

📅
datetime
The calendar
Structured objects for dates, times, and both together. Comparing dates, formatting them, doing arithmetic ("30 days from now"). This is the module you'll use 90% of the time.
time
The stopwatch & ticker tape
Lower-level: time.time() gives the current Unix timestamp, time.sleep() pauses execution, time.perf_counter() measures elapsed time precisely. Use for timing code and low-level Unix time.
📆
calendar
The month view
Pure calendar operations: is this a leap year? What day of the week was January 1st, 1970? Prints text calendars. Rarely needed for typical work — but handy when it is.
# Quick tour of "what time is it right now?" from each angle
from datetime import datetime
import time

# As a datetime object
now = datetime.now()
print(now)                     # 2026-07-12 14:30:15.123456

# As a Unix timestamp (seconds since 1970)
print(time.time())              # 1752324615.123

# As an ISO-8601 string
print(now.isoformat())         # 2026-07-12T14:30:15.123456

# As a formatted string of your choosing
print(now.strftime("%A, %d %B %Y"))   # Sunday, 12 July 2026

Section 03

Understanding the Unix Epoch

A Unix timestamp (also called epoch time) is the number of seconds that have elapsed since a fixed reference point: midnight UTC on January 1st, 1970. That's the "epoch." Every timestamp you see — 1752324615 — is just that many seconds after 1970-01-01.

Why 1970? Unix was born around then, and the engineers who designed the operating system needed a simple way to store time. A single integer counting seconds is the simplest possible representation — no time zones, no leap years, no strings. Databases, log files, APIs, filesystems: they all still speak epoch time.

The Unix Epoch Timeline
0 THE EPOCH Jan 1, 1970 UTC Y2K Jan 1, 2000 946,684,800 1 BILLION SEC Sep 9, 2001 1,000,000,000 2020 Jan 1, 2020 1,577,836,800 TODAY Jul 12, 2026 ~1,784,246,400 Y2038 PROBLEM Jan 19, 2038 2,147,483,647 ← negative — rare (before 1970) every second: +1 →

Every timestamp is the number of seconds since the red dot. Today is around 1.78 billion. The purple dot marks the "Y2038 problem" — when 32-bit signed integers overflow. Modern systems use 64-bit and are safe.


Section 04

The Three-Way Conversion Triangle

This is the mental model to internalise. Any moment can be represented as a human string, a datetime object, or a Unix timestamp — and Python gives you a named function for each arrow between them.

The Three Faces of a Moment — and How to Convert
HUMAN STRING "2026-07-12 14:30:00" what users read & write DATETIME OBJECT datetime(2026,7,12, 14, 30, 0) what you compute with UNIX TIMESTAMP 1752341400.0 what databases & APIs store datetime.strptime() "parse: string parse" .strftime() "format: string format" via datetime (two-step) .timestamp() .fromtimestamp()

The datetime object is the hub. Strings and timestamps convert to each other by going through it. Learn the four solid arrows and every date conversion in Python is a two-step recipe.


Section 05

Human String ↔ datetime

String → datetime with strptime()

strptime stands for "string parse time." You give it a string and a format that describes exactly how the string is laid out. Python matches the pattern and returns a datetime object.

from datetime import datetime

# The format string uses %-codes to describe each part
dt = datetime.strptime("2026-07-12 14:30:00", "%Y-%m-%d %H:%M:%S")
print(dt)                  # 2026-07-12 14:30:00
print(type(dt))            # <class 'datetime.datetime'>

# Different string, different format
dt2 = datetime.strptime("12/Jul/2026", "%d/%b/%Y")
print(dt2)                 # 2026-07-12 00:00:00

# Once it's a datetime object, you can query anything
print(dt.year, dt.month, dt.day)      # 2026 7 12
print(dt.weekday())                    # 6 (Sunday, 0=Monday)

datetime → String with strftime()

strftime stands for "string format time." Same %-code vocabulary as strptime, but working the other direction — turn a datetime into any string layout you want.

from datetime import datetime

now = datetime.now()

# Common formats you'll actually use
print(now.strftime("%Y-%m-%d"))            # 2026-07-12          (ISO date)
print(now.strftime("%Y-%m-%d %H:%M:%S"))   # 2026-07-12 14:30:15 (ISO datetime)
print(now.strftime("%d/%m/%Y"))            # 12/07/2026          (UK date)
print(now.strftime("%m/%d/%Y"))            # 07/12/2026          (US date)

# Human-friendly formats
print(now.strftime("%A, %d %B %Y"))        # Sunday, 12 July 2026
print(now.strftime("%d %b '%y"))           # 12 Jul '26
print(now.strftime("%I:%M %p"))            # 02:30 PM (12-hour)

# For filenames — safe characters only
print(now.strftime("backup_%Y%m%d_%H%M%S.zip"))
# backup_20260712_143015.zip
🔑
The Memory Trick

strptimeparse — reads a string, returns a datetime.
strftimeformat — reads a datetime, returns a string.
p for parse, f for format. Never confuse them again.


Section 06

The Format Code Reference

Both strptime and strftime use the same %-code vocabulary. Here are the ones you'll use 95% of the time.

CodeMeaningExample
%Y4-digit year2026
%y2-digit year26
%mMonth as number, zero-padded07
%BFull month nameJuly
%bAbbreviated month nameJul
%dDay of month, zero-padded12
%AFull weekday nameSunday
%aAbbreviated weekdaySun
%HHour (24-hour), zero-padded14
%IHour (12-hour), zero-padded02
%MMinute, zero-padded30
%SSecond, zero-padded15
%pAM or PMPM
%fMicroseconds (6 digits)123456
%jDay of year (001-366)193
%ZTimezone nameUTC
%zUTC offset+0000
%%A literal % character%

Section 07

datetime ↔ Unix Timestamp

datetime → Timestamp with .timestamp()

from datetime import datetime

dt = datetime.now()
ts = dt.timestamp()

print(ts)              # 1752324615.123 — seconds since 1970-01-01
print(type(ts))        # <class 'float'>

# Cast to int if you don't need sub-second precision
print(int(ts))         # 1752324615

Timestamp → datetime with .fromtimestamp()

from datetime import datetime

# Local time interpretation (uses your machine's timezone)
dt = datetime.fromtimestamp(1752341400)
print(dt)                                  # 2026-07-12 14:30:00 (local time)

# UTC interpretation — recommended for stored/shared timestamps
from datetime import timezone
dt_utc = datetime.fromtimestamp(1752341400, tz=timezone.utc)
print(dt_utc)                              # 2026-07-12 14:30:00+00:00
⚠️
Local vs UTC — The Silent Bug Zone

datetime.now() gives local time. datetime.utcnow() gives UTC. fromtimestamp() converts to local by default, fromtimestamp(ts, tz=timezone.utc) converts to UTC. When storing or transmitting timestamps, always use UTC. Save local time to a database and your data breaks the moment the server timezone changes.


Section 08

The Full Recipe — String ↔ Epoch In One Go

This is what most people actually need: turn a human-typed date directly into a Unix timestamp, or turn a Unix timestamp from a database directly into any displayable format. Both are two-step operations through the datetime hub.

Human String → Unix Timestamp

from datetime import datetime, timezone

def to_epoch(text, fmt="%Y-%m-%d %H:%M:%S"):
    """Convert a human-readable date string to a UTC Unix timestamp."""
    dt = datetime.strptime(text, fmt)
    dt = dt.replace(tzinfo=timezone.utc)   # treat as UTC
    return int(dt.timestamp())

print(to_epoch("2026-07-12 14:30:00"))
# 1752330600

print(to_epoch("12/Jul/2026 14:30", fmt="%d/%b/%Y %H:%M"))
# 1752330600

print(to_epoch("2000-01-01 00:00:00"))       # Y2K
# 946684800

Unix Timestamp → Human String (Any Format)

from datetime import datetime, timezone

def from_epoch(ts, fmt="%Y-%m-%d %H:%M:%S", utc=True):
    """Convert a Unix timestamp to a formatted human-readable string."""
    if utc:
        dt = datetime.fromtimestamp(ts, tz=timezone.utc)
    else:
        dt = datetime.fromtimestamp(ts)         # local time
    return dt.strftime(fmt)

ts = 1752330600

print(from_epoch(ts))
# 2026-07-12 14:30:00

print(from_epoch(ts, "%A, %d %B %Y at %I:%M %p"))
# Sunday, 12 July 2026 at 02:30 PM

print(from_epoch(ts, "%d/%m/%Y"))
# 12/07/2026

print(from_epoch(ts, "backup_%Y%m%d_%H%M%S.log"))
# backup_20260712_143000.log
This Is 90% Of Real-World Date Code

Read a timestamp from a database or API → convert to a datetime → format for display. Read a user's input → parse to a datetime → convert to timestamp → save to database. Master these two functions and you've handled almost every practical scenario.


Section 09

The time Module — Low-Level Essentials

import time

# Current Unix timestamp (fastest way — no datetime object created)
print(time.time())            # 1752324615.789

# Sleep — pause execution
time.sleep(2.5)                # waits 2.5 seconds

# High-precision timer — for benchmarking
start = time.perf_counter()
# ... some code ...
elapsed = time.perf_counter() - start
print(f"Elapsed: {elapsed:.4f}s")

# Convert Unix timestamp to a "struct_time" — old-school C-style
t = time.gmtime(1752341400)         # UTC
print(t.tm_year, t.tm_mon, t.tm_mday)   # 2026 7 12

# Format a struct_time — same %-codes as datetime
print(time.strftime("%Y-%m-%d %H:%M:%S", t))
# 2026-07-12 14:30:00
📈
time.time() vs time.perf_counter()

time.time() is the "wall clock" — real-world time. It can jump backward if the system clock is adjusted. time.perf_counter() is monotonic — it never goes backward — but has no meaning as a date. Use time() for timestamps, perf_counter() for measuring how long code took.


Section 10

Timezones — Naive vs Aware Datetimes

A naive datetime doesn't know its own timezone. An aware datetime does. This distinction is the #1 source of subtle date bugs — mixing the two raises TypeError.

❌ Naive Datetime
TraitDetail
TimezoneNone (unspecified)
AssumesWhatever you think it does — danger
Exampledatetime(2026, 7, 12, 14, 30)
Safe forWall-clock displays only
✅ Aware Datetime
TraitDetail
TimezoneExplicit — e.g. UTC, US/Pacific
AssumesNothing — encoded in the object
Exampledatetime(..., tzinfo=timezone.utc)
Safe forStorage, transmission, calculations
from datetime import datetime, timezone, timedelta

# NAIVE — no timezone attached
naive = datetime.now()
print(naive)                # 2026-07-12 14:30:15.123 (no tz info)
print(naive.tzinfo)          # None

# AWARE — explicitly UTC
aware = datetime.now(timezone.utc)
print(aware)                # 2026-07-12 14:30:15.123+00:00
print(aware.tzinfo)          # datetime.timezone.utc

# AWARE — a specific offset (IST = UTC+5:30)
ist = timezone(timedelta(hours=5, minutes=30))
now_ist = datetime.now(ist)
print(now_ist)              # 2026-07-12 20:00:15.123+05:30

# CONVERTING between timezones (only works on AWARE datetimes)
utc_moment = datetime.now(timezone.utc)
ist_moment = utc_moment.astimezone(ist)
print(f"UTC: {utc_moment}")
print(f"IST: {ist_moment}")

# Modern Python 3.9+ — proper named timezones via zoneinfo
from zoneinfo import ZoneInfo
tokyo = datetime.now(ZoneInfo("Asia/Tokyo"))
london = datetime.now(ZoneInfo("Europe/London"))
print(tokyo)
print(london)

Section 11

Time Arithmetic with timedelta

timedelta represents a duration — a span of time. Add or subtract it from a datetime to move forward or backward.

from datetime import datetime, timedelta

now = datetime.now()

# Move forward / backward
tomorrow      = now + timedelta(days=1)
next_week     = now + timedelta(weeks=1)
three_hours   = now + timedelta(hours=3)
in_90_minutes = now + timedelta(minutes=90)
yesterday     = now - timedelta(days=1)

# Complex durations — all fields sum together
custom = timedelta(days=2, hours=3, minutes=30, seconds=45)
print(now + custom)

# Difference between two datetimes IS a timedelta
event = datetime(2026, 12, 25)
until = event - now
print(f"Days until Christmas: {until.days}")
print(f"Total seconds:        {until.total_seconds():.0f}")
OUTPUT
Days until Christmas: 166 Total seconds: 14342400

Section 12

Practical Real-World Examples

Example 1 — Age From Date of Birth

from datetime import datetime

def age(dob_string):
    dob = datetime.strptime(dob_string, "%Y-%m-%d")
    today = datetime.today()
    years = today.year - dob.year
    # Adjust if birthday hasn't happened yet this year
    if (today.month, today.day) < (dob.month, dob.day):
        years -= 1
    return years

print(age("1990-03-15"))    # 36

Example 2 — Countdown Timer

from datetime import datetime

def countdown(target_string):
    target = datetime.strptime(target_string, "%Y-%m-%d %H:%M")
    delta  = target - datetime.now()
    if delta.total_seconds() < 0:
        return "That moment has passed!"

    days = delta.days
    hours, rem = divmod(delta.seconds, 3600)
    minutes = rem // 60
    return f"{days}d {hours}h {minutes}m remaining"

print(countdown("2026-12-31 23:59"))
# 172d 9h 28m remaining

Example 3 — Parse Log Timestamps

from datetime import datetime

log_lines = [
    "[2026-07-12 14:30:15] User logged in",
    "[2026-07-12 14:31:02] File uploaded",
    "[2026-07-12 14:35:47] Session expired",
]

events = []
for line in log_lines:
    # Extract timestamp between the brackets
    ts_str = line[1:20]
    dt = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
    events.append((dt, line[22:]))

# Time between first and last event
duration = events[-1][0] - events[0][0]
print(f"Session lasted {duration.total_seconds():.0f} seconds")

Example 4 — Convert Between Systems

from datetime import datetime, timezone

# API returned this JSON: {"created_at": 1752341400}
epoch_from_api = 1752341400

# Convert to human-readable for display
dt = datetime.fromtimestamp(epoch_from_api, tz=timezone.utc)
print(f"Created: {dt.strftime('%d %b %Y, %H:%M UTC')}")
# Created: 12 Jul 2026, 14:30 UTC

# User types "31/12/2026" — save it back to database as epoch
user_input = "31/12/2026"
dt_user = datetime.strptime(user_input, "%d/%m/%Y")
dt_user = dt_user.replace(tzinfo=timezone.utc)
epoch_to_save = int(dt_user.timestamp())
print(f"Saving: {epoch_to_save}")
# Saving: 1798761600

Example 5 — Timestamped Filenames

from datetime import datetime

# Perfect for backups, exports, unique filenames
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"export_{timestamp}.csv"
print(filename)
# export_20260712_143015.csv

# More human-readable variant
filename2 = datetime.now().strftime("backup-%Y-%m-%d.tar.gz")
print(filename2)
# backup-2026-07-12.tar.gz

Section 13

Common Pitfalls

⚠️
Mixing Naive & Aware
TypeError on subtraction
You can't subtract a naive datetime from an aware one — Python raises TypeError. Pick one style per codebase (aware, always UTC is best) and stick to it.
🚫
%m vs %M
Month vs Minute
Lowercase %m is the month. Uppercase %M is the minute. Swap them and your dates land 6 months from when they should. Silent bug of the worst kind — code runs, output looks plausible.
📌
Local Time in Storage
Timezone-dependent data
Storing local-time datetimes in databases breaks the moment a user or server moves timezones. Store UTC (or the epoch integer) and format to local only at the display layer.
🛠
Timestamp is Float, Not Int
.timestamp() returns 1752341400.123
Wrap in int() if you want a whole-second integer for storage or comparison. Otherwise sub-second drift causes equality checks to fail mysteriously.
🔮
strptime Is Strict
Format must match EXACTLY
strptime("2026-7-12", "%Y-%m-%d") fails because the month isn't zero-padded. Use libraries like dateutil for free-form parsing when the input format is unpredictable.
🔥
DST & Ambiguous Times
2:30 AM might not exist
When clocks spring forward, some local times are skipped; when they fall back, some local times occur twice. Working in UTC internally sidesteps this entirely — one more reason to keep timezone awareness at the edges.

Section 14

Golden Rules

🔑 Date & Time — Non-Negotiable Rules
1
Store and transmit dates as UTC — always. Epoch timestamps or timezone-aware UTC datetimes only. Convert to local time only at the display layer, right before the user sees it. This one rule prevents an entire class of bugs.
2
Remember: strptime parses, strftime formats. p for parse (string in, datetime out). f for format (datetime in, string out). The mnemonic never fails.
3
Never mix naive and aware datetimes. Pick one per codebase. For new projects, always use aware datetimes with UTC as the default. Naive datetimes should be a red flag in any code review.
4
Watch %m versus %M. Lowercase is month, uppercase is minute. Similarly %d (day) versus %D (US-format date). Case-sensitivity in format codes causes the sneakiest date bugs in Python.
5
Use time.time() for timestamps, time.perf_counter() for measuring durations. The wall clock can jump backward; the performance counter never does. Using the wrong one makes benchmarks lie.
6
For unknown or free-form date formats, reach for python-dateutil. dateutil.parser.parse() handles almost any human input. Save strptime for cases where you control the input format exactly.
7
Cast .timestamp() to int when saving. Sub-second precision is rarely needed for storage and causes equality checks to silently fail. Whole seconds compare cleanly and take less space.