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:

Python — the dangerous pattern
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.

✗ Avoid
datetime.fromtimestamp(ts)
Uses the local system timezone. Different result on different machines. Naive datetime.
✗ Deprecated
datetime.utcfromtimestamp(ts)
Returns UTC values but creates a naive datetime. Deprecated in Python 3.12.
✓ Correct
datetime.fromtimestamp(ts, tz=timezone.utc)
Returns a timezone-aware UTC datetime. Same result everywhere. The right choice.
✓ Also Correct
datetime.fromtimestamp(ts, tz=ZoneInfo("Europe/Paris"))
Converts directly to any timezone, timezone-aware. Use zoneinfo (Python 3.9+).
Python — the correct pattern
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)

Python — handling seconds, milliseconds, and strings
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

Python — convert datetime back to 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:

CodeMeaningExample
%Y4-digit year2026
%mMonth, zero-padded05
%dDay, zero-padded11
%HHour (24h), zero-padded14
%MMinute, zero-padded38
%SSecond, zero-padded32
%AFull weekday nameMonday
%BFull month nameMay
%ZTimezone abbreviationUTC
%zUTC offset+0000
%fMicroseconds000000
Python — strftime examples
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.

Python 3.9+ — zoneinfo (recommended)
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"
Python — pytz (for older Python versions)
# 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:

Python — ISO 8601 string → 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.

Python — date arithmetic
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.

Python — pandas timestamps
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() without tz= — 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. Add tzinfo=timezone.utc first with .replace().
  • Using datetime.now() instead of datetime.now(tz=timezone.utc) — always pass the timezone argument.
  • Mixing naive and aware datetimes — comparing or subtracting a naive and aware datetime raises a TypeError in Python. Keep everything aware.
  • Storing as float instead of inttime.time() returns a float. Always wrap with int() before storing as a Unix timestamp.

Quick Reference

GoalPython code
Current Unix secondsint(time.time())
Current UTC datetimedatetime.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 secondsint(dt.timestamp())
datetime → ISO 8601dt.isoformat()
ISO 8601 string → datetimedatetime.fromisoformat(s.replace("Z","+00:00"))
Convert timezonedt.astimezone(ZoneInfo("Europe/Paris"))
Format as stringdt.strftime("%Y-%m-%d %H:%M:%S")
Add timedt + 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.