The Story That Explains Time in Python
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.
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.
The Time Landscape — Python's Three Modules
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.
# 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
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.
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.
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 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.
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
strptime — parse — reads a string,
returns a datetime.
strftime — format — reads a
datetime, returns a string.
p for parse, f for format. Never confuse them again.
The Format Code Reference
Both strptime and strftime use the same
%-code vocabulary. Here are the ones you'll use 95% of the time.
| Code | Meaning | Example |
|---|---|---|
%Y | 4-digit year | 2026 |
%y | 2-digit year | 26 |
%m | Month as number, zero-padded | 07 |
%B | Full month name | July |
%b | Abbreviated month name | Jul |
%d | Day of month, zero-padded | 12 |
%A | Full weekday name | Sunday |
%a | Abbreviated weekday | Sun |
%H | Hour (24-hour), zero-padded | 14 |
%I | Hour (12-hour), zero-padded | 02 |
%M | Minute, zero-padded | 30 |
%S | Second, zero-padded | 15 |
%p | AM or PM | PM |
%f | Microseconds (6 digits) | 123456 |
%j | Day of year (001-366) | 193 |
%Z | Timezone name | UTC |
%z | UTC offset | +0000 |
%% | A literal % character | % |
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
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.
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
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.
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() 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.
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.
| Trait | Detail |
|---|---|
| Timezone | None (unspecified) |
| Assumes | Whatever you think it does — danger |
| Example | datetime(2026, 7, 12, 14, 30) |
| Safe for | Wall-clock displays only |
| Trait | Detail |
|---|---|
| Timezone | Explicit — e.g. UTC, US/Pacific |
| Assumes | Nothing — encoded in the object |
| Example | datetime(..., tzinfo=timezone.utc) |
| Safe for | Storage, 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)
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}")
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
Common Pitfalls
TypeError. Pick one style per codebase (aware, always UTC
is best) and stick to it.
%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.
int() if you want a whole-second integer for storage
or comparison. Otherwise sub-second drift causes equality checks to fail
mysteriously.
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.
Golden Rules
%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.
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.
python-dateutil. dateutil.parser.parse()
handles almost any human input. Save strptime for cases where
you control the input format exactly.
.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.