Convert Unix Timestamp to Date in Python — Complete Guide
Python has three ways to convert a Unix timestamp to a datetime — and two of them will silently give you the wrong answer in production. This guide covers every method, every pitfall, and the patterns you should actually use.
The Method You're Probably Using Is Wrong
Most Python tutorials show you this:
from datetime import datetime
# ❌ This looks fine but is broken in production
dt = datetime.fromtimestamp(1715429912)
print(dt)
# → 2026-05-11 16:38:32 ← but WHICH timezone?
# ❌ This is deprecated and always wrong
dt = datetime.utcfromtimestamp(1715429912)
print(dt)
# → 2026-05-11 14:38:32 ← no timezone info, naive datetime
Both produce naive datetimes — datetime objects with no timezone information. When you compare them, add them, or store them, Python has no idea what timezone they're in. On a server in a different timezone than your laptop, fromtimestamp() gives a completely different result.
Python 3.12+ deprecation: datetime.utcfromtimestamp() is officially deprecated and will be removed in a future version. Stop using it. The replacement is datetime.fromtimestamp(ts, tz=timezone.utc).
The Correct Pattern — Always Use timezone.utc
The single rule that eliminates all timezone bugs: always pass tz=timezone.utc when converting from a Unix timestamp.
from datetime import datetime, timezone
ts = 1715429912 # Unix seconds from your API / database
# ✅ Always use tz=timezone.utc — same result everywhere
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
print(dt)
# → 2026-05-11 14:38:32+00:00
print(dt.isoformat())
# → 2026-05-11T14:38:32+00:00
print(dt.strftime("%Y-%m-%d %H:%M:%S UTC"))
# → 2026-05-11 14:38:32 UTC
Unix Seconds → datetime (All Cases)
from datetime import datetime, timezone
# From Unix seconds (most APIs)
dt = datetime.fromtimestamp(1715429912, tz=timezone.utc)
# From Unix milliseconds (JavaScript Date.now(), Java)
ms = 1715429912000
dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
# From Unix microseconds (PostgreSQL, ClickHouse)
us = 1715429912000000
dt = datetime.fromtimestamp(us / 1_000_000, tz=timezone.utc)
# From string (API gives you "1715429912" as a string)
dt = datetime.fromtimestamp(int("1715429912"), tz=timezone.utc)
# Normalize any precision automatically
def from_any_unix(ts) -> datetime:
ts = int(ts)
digits = len(str(abs(ts)))
if digits <= 10: seconds = ts
elif digits <= 13: seconds = ts / 1_000
elif digits <= 16: seconds = ts / 1_000_000
else: seconds = ts / 1_000_000_000
return datetime.fromtimestamp(seconds, tz=timezone.utc)
from_any_unix(1715429912) # seconds ✅
from_any_unix(1715429912000) # milliseconds ✅
from_any_unix("1715429912") # string ✅
datetime → Unix Timestamp
from datetime import datetime, timezone
# Current time as Unix seconds
now_sec = int(datetime.now(tz=timezone.utc).timestamp())
# or simply:
import time
now_sec = int(time.time())
# Specific datetime → Unix seconds
dt = datetime(2026, 5, 11, 14, 38, 32, tzinfo=timezone.utc)
unix_sec = int(dt.timestamp()) # → 1715429912
unix_ms = int(dt.timestamp() * 1000) # → 1715429912000
# ⚠️ If your datetime is naive (no timezone), .timestamp() uses LOCAL time
# This gives different results on different machines!
naive_dt = datetime(2026, 5, 11, 14, 38, 32) # no tzinfo
# naive_dt.timestamp() ← result depends on system timezone ❌
# Always add timezone before calling .timestamp()
aware_dt = naive_dt.replace(tzinfo=timezone.utc)
aware_dt.timestamp() # → 1715429912.0 ✅
Format datetime with strftime
Python's strftime() formats a datetime to any string pattern you need. Here are the most useful format codes:
| Code | Meaning | Example |
|---|---|---|
%Y | 4-digit year | 2026 |
%m | Month, zero-padded | 05 |
%d | Day, zero-padded | 11 |
%H | Hour (24h), zero-padded | 14 |
%M | Minute, zero-padded | 38 |
%S | Second, zero-padded | 32 |
%A | Full weekday name | Monday |
%B | Full month name | May |
%Z | Timezone abbreviation | UTC |
%z | UTC offset | +0000 |
%f | Microseconds | 000000 |
from datetime import datetime, timezone
dt = datetime.fromtimestamp(1715429912, tz=timezone.utc)
# Common formats
dt.strftime("%Y-%m-%d") # → "2026-05-11"
dt.strftime("%Y-%m-%d %H:%M:%S") # → "2026-05-11 14:38:32"
dt.strftime("%d %B %Y") # → "11 May 2026"
dt.strftime("%A, %B %d %Y") # → "Monday, May 11 2026"
dt.strftime("%I:%M %p") # → "02:38 PM"
dt.isoformat() # → "2026-05-11T14:38:32+00:00"
# Parse a string back to datetime (strptime)
dt2 = datetime.strptime("2026-05-11 14:38:32", "%Y-%m-%d %H:%M:%S")
# ⚠️ strptime produces a naive datetime — add timezone after!
dt2 = dt2.replace(tzinfo=timezone.utc)
Timezone Conversion — zoneinfo (Python 3.9+)
Python 3.9 introduced zoneinfo as a built-in stdlib module, replacing the need for pytz in most cases. Always convert from a UTC-aware datetime — never from a naive one.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo # Python 3.9+ stdlib
ts = 1715429912
# Method 1: convert to UTC first, then to target timezone
dt_utc = datetime.fromtimestamp(ts, tz=timezone.utc)
dt_paris = dt_utc.astimezone(ZoneInfo("Europe/Paris"))
dt_tokyo = dt_utc.astimezone(ZoneInfo("Asia/Tokyo"))
dt_nyc = dt_utc.astimezone(ZoneInfo("America/New_York"))
print(dt_paris) # → 2026-05-11 16:38:32+02:00
print(dt_tokyo) # → 2026-05-11 23:38:32+09:00
print(dt_nyc) # → 2026-05-11 10:38:32-04:00
# Method 2: pass timezone directly to fromtimestamp()
dt_paris = datetime.fromtimestamp(ts, tz=ZoneInfo("Europe/Paris"))
# Get the UTC offset as a string
offset = dt_paris.strftime("%z") # → "+0200"
tz_name = dt_paris.strftime("%Z") # → "CEST"
# pip install pytz (only needed for Python < 3.9)
import pytz
from datetime import datetime
ts = 1715429912
paris = pytz.timezone("Europe/Paris")
dt = datetime.fromtimestamp(ts, tz=pytz.utc).astimezone(paris)
print(dt) # → 2026-05-11 16:38:32+02:00
# ⚠️ pytz gotcha: always use astimezone(), never replace()
# naive_dt.replace(tzinfo=paris) ← wrong, gives LMT offset on some dates
Parse ISO 8601 Strings
When your API returns an ISO 8601 string like "2026-05-11T14:38:32Z" and you need a Unix timestamp:
from datetime import datetime, timezone
# Python 3.11+: fromisoformat() handles 'Z' suffix natively
dt = datetime.fromisoformat("2026-05-11T14:38:32Z") # Python 3.11+
dt = datetime.fromisoformat("2026-05-11T14:38:32+00:00")
# Python 3.7–3.10: fromisoformat() doesn't handle 'Z'
iso = "2026-05-11T14:38:32Z"
dt = datetime.fromisoformat(iso.replace("Z", "+00:00"))
# → Unix seconds
unix_sec = int(dt.timestamp()) # → 1715429912
# Parse with offset (e.g. "2026-05-11T16:38:32+02:00")
dt2 = datetime.fromisoformat("2026-05-11T16:38:32+02:00")
unix_sec = int(dt2.timestamp()) # → still 1715429912 ✅
Date Arithmetic with timedelta
Python's timedelta makes date math clean and readable. Always do arithmetic on timezone-aware datetimes.
from datetime import datetime, timezone, timedelta
now = datetime.now(tz=timezone.utc)
# Add and subtract time
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(weeks=1)
in_2_hours = now + timedelta(hours=2)
plus_90_min = now + timedelta(minutes=90)
# Difference between two datetimes
dt1 = datetime.fromtimestamp(1715429912, tz=timezone.utc)
dt2 = datetime.now(tz=timezone.utc)
delta = dt2 - dt1 # timedelta object
print(delta.days) # total days
print(delta.total_seconds()) # total seconds (float)
# Check if a datetime is in the past
is_past = dt1 < datetime.now(tz=timezone.utc)
# JWT-style: create exp timestamp for +1 hour
exp_dt = datetime.now(tz=timezone.utc) + timedelta(hours=1)
exp_unix = int(exp_dt.timestamp()) # Unix seconds
Verify your Python conversions instantly
Paste any Unix timestamp into UnixLi — it shows the UTC datetime, local time, ISO 8601, SQL format, relative time, and generates the Python snippet automatically.
Open UnixLi →Pandas Timestamps
If you're working with data pipelines or DataFrames, pandas has its own timestamp handling that's slightly different from standard Python datetime.
import pandas as pd
# Unix seconds → pandas Timestamp (UTC)
ts = pd.Timestamp(1715429912, unit="s", tz="UTC")
# → Timestamp('2026-05-11 14:38:32+0000', tz='UTC')
# Unix milliseconds → Timestamp
ts = pd.Timestamp(1715429912000, unit="ms", tz="UTC")
# pandas Timestamp → Unix seconds
unix_sec = int(ts.timestamp())
# or:
unix_sec = ts.value // 10**9 # ts.value is always nanoseconds
# DataFrame column: convert Unix timestamps to datetime
df["created_at"] = pd.to_datetime(df["created_at_unix"], unit="s", utc=True)
# Convert datetime column back to Unix seconds
df["unix"] = df["created_at"].astype("int64") // 10**9
Common Python Timestamp Mistakes
- Using
fromtimestamp()withouttz=— uses the server's local timezone. Your code works on your laptop (UTC+2) and silently breaks in production (UTC). - Using
utcfromtimestamp()— returns correct UTC values but creates a naive datetime. Deprecated in Python 3.12. - Calling
.timestamp()on a naive datetime — Python assumes it's in local time. Addtzinfo=timezone.utcfirst with.replace(). - Using
datetime.now()instead ofdatetime.now(tz=timezone.utc)— always pass the timezone argument. - Mixing naive and aware datetimes — comparing or subtracting a naive and aware datetime raises a
TypeErrorin Python. Keep everything aware. - Storing as float instead of int —
time.time()returns a float. Always wrap withint()before storing as a Unix timestamp.
Quick Reference
| Goal | Python code |
|---|---|
| Current Unix seconds | int(time.time()) |
| Current UTC datetime | datetime.now(tz=timezone.utc) |
| Unix seconds → datetime (UTC) | datetime.fromtimestamp(ts, tz=timezone.utc) |
| Unix ms → datetime (UTC) | datetime.fromtimestamp(ms/1000, tz=timezone.utc) |
| datetime → Unix seconds | int(dt.timestamp()) |
| datetime → ISO 8601 | dt.isoformat() |
| ISO 8601 string → datetime | datetime.fromisoformat(s.replace("Z","+00:00")) |
| Convert timezone | dt.astimezone(ZoneInfo("Europe/Paris")) |
| Format as string | dt.strftime("%Y-%m-%d %H:%M:%S") |
| Add time | dt + timedelta(hours=1) |
| Difference in seconds | (dt2 - dt1).total_seconds() |
| Is in the past? | dt < datetime.now(tz=timezone.utc) |
Summary
The one rule that fixes 90% of Python timestamp bugs: always pass tz=timezone.utc to every datetime function that accepts a timezone argument. This applies to fromtimestamp(), now(), and replace(). The moment you work with timezone-aware datetimes throughout your codebase, the bugs disappear and the results become deterministic across every environment.
For more timestamp guides: JavaScript timestamps, JWT expiration in Python, and seconds vs milliseconds.