Working with Unix Timestamps in Python: datetime, arrow, and beyond

Published pythonunix-timestampdatetimebackend

You call an API, get back a number like 1712700000, and then the fun begins. Do you pass it to datetime.fromtimestamp() or datetime.utcfromtimestamp()? Is the result timezone-aware or naive? What happens when the timestamp is in milliseconds instead of seconds? And why does the same code produce different output on your laptop versus the server?

Python’s standard library gives you enough rope to hang yourself. Between naive and aware datetimes, pytz vs zoneinfo, and the silent seconds/milliseconds mismatch, timestamp handling trips up developers at every experience level. This guide gives you the patterns that are correct by default, the recipes you can drop into any project, and the pitfalls to watch for before they reach production.


The Core Concepts: Naive vs Aware, Seconds vs Milliseconds

fromtimestamp() vs utcfromtimestamp() — a critical difference

from datetime import datetime

ts = 1712700000

# ❌ Naive — uses your LOCAL system timezone
dt_local = datetime.fromtimestamp(ts)
# → datetime(2024, 4, 9, 16, 0, 0)  on a UTC-4 machine
# → datetime(2024, 4, 9, 20, 0, 0)  on a UTC machine
# Same input, different output. No timezone info attached.

# ❌ Also naive — returns UTC but attaches NO tzinfo
dt_utc_naive = datetime.utcfromtimestamp(ts)
# → datetime(2024, 4, 9, 20, 0, 0)
# Looks like UTC but the object has no idea it is UTC.

Both of the above produce naive datetimes — objects with no timezone context. Pass them to another function that assumes UTC and you may silently get wrong results.

The correct pattern: timezone-aware from the start

from datetime import datetime, timezone

ts = 1712700000

# ✅ Timezone-aware — always correct, always explicit
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
# → datetime(2024, 4, 9, 20, 0, 0, tzinfo=timezone.utc)

The tz=timezone.utc argument is the single change that makes everything safe.

Converting a datetime back to epoch

dt = datetime(2024, 4, 9, 20, 0, 0, tzinfo=timezone.utc)
epoch = dt.timestamp()
# → 1712700000.0

If dt is naive, .timestamp() assumes local time — another silent source of bugs.

Seconds vs milliseconds

Unix timestamps are defined in seconds, but many APIs (JavaScript-originated ones especially) return milliseconds. A 13-digit number is almost certainly milliseconds; a 10-digit number is almost certainly seconds.

ts_ms = 1712700000000   # milliseconds — from a JS API, database column, etc.
ts_s  = ts_ms / 1000    # → 1712700000.0 (seconds)

Three Practical Recipes

Recipe 1 — Safe epoch-to-datetime conversion

A reusable helper that handles both seconds and milliseconds and always returns a timezone-aware result:

from datetime import datetime, timezone

def epoch_to_dt(ts: int | float, *, ms: bool = False) -> datetime:
    """Convert Unix timestamp to timezone-aware UTC datetime.

    Args:
        ts: Unix timestamp (seconds or milliseconds)
        ms: Set True if timestamp is in milliseconds
    """
    seconds = ts / 1000 if ms else ts
    return datetime.fromtimestamp(seconds, tz=timezone.utc)

# Usage
epoch_to_dt(1712700000)               # → 2024-04-09 20:00:00+00:00
epoch_to_dt(1712700000000, ms=True)   # → 2024-04-09 20:00:00+00:00

The ms keyword-only argument makes the intent explicit at every call site — no guessing from the reader.

Recipe 2 — Converting to a named timezone with pytz

timezone.utc covers UTC, but real applications need named timezones like America/New_York or Europe/Berlin. pytz handles this:

import pytz
from datetime import datetime

tz_ny = pytz.timezone("America/New_York")

dt_utc = datetime.fromtimestamp(1712700000, tz=pytz.utc)
dt_ny  = dt_utc.astimezone(tz_ny)

print(dt_ny)   # → 2024-04-09 16:00:00-04:00
print(dt_ny.strftime("%B %d, %Y %I:%M %p %Z"))
# → April 09, 2024 04:00 PM EDT

Key point: always start with a UTC-aware datetime (tz=pytz.utc), then convert with .astimezone(). Never localize a naive datetime derived from a Unix timestamp.

If you are on Python 3.9+ and prefer the standard library, zoneinfo is a drop-in alternative:

from zoneinfo import ZoneInfo
from datetime import datetime, timezone

dt_utc = datetime.fromtimestamp(1712700000, tz=timezone.utc)
dt_ny  = dt_utc.astimezone(ZoneInfo("America/New_York"))
print(dt_ny)   # → 2024-04-09 16:00:00-04:00

Recipe 3 — With arrow (cleaner API)

arrow is a third-party library that wraps the full workflow in a concise API. Worth it if your project already uses it or if you find the standard library verbose:

import arrow

# Seconds
dt = arrow.get(1712700000)
print(dt.to("America/New_York").format("YYYY-MM-DD HH:mm:ss ZZ"))
# → 2024-04-09 16:00:00 -04:00

# Milliseconds — divide first (arrow expects seconds)
dt_ms = arrow.get(1712700000000 / 1000)
print(dt_ms.isoformat())
# → 2024-04-09T20:00:00+00:00

# Back to epoch
dt.timestamp()   # → 1712700000.0

arrow is especially handy for formatting and humanizing (dt.humanize()"2 years ago"), but for pure conversion tasks the standard library with timezone.utc is zero-dependency and sufficient.


Verify Your Output Instantly

Paste any timestamp directly into our epoch to datetime converter to verify your Python output matches — no environment needed.


Bonus: Timestamps in pandas

Data pipelines frequently store epochs in DataFrame columns. pd.to_datetime() handles the conversion cleanly:

import pandas as pd

# Integer epoch column → timezone-aware datetime column
df["created_at"] = pd.to_datetime(df["epoch_col"], unit="s", utc=True)
# unit="s" for seconds, unit="ms" for milliseconds

# datetime column → epoch (seconds)
df["epoch_back"] = df["created_at"].astype("int64") // 10**9

The unit parameter is the key — it defaults to nanoseconds in some contexts, so always be explicit. utc=True ensures the resulting column is timezone-aware (DatetimeTZDtype with UTC), which avoids the same naive-datetime pitfalls as the standard library.

For millisecond epoch columns, change unit="s" to unit="ms" and adjust the back-conversion divisor to 10**6.


Further Reading

Try it in your browser

No setup needed — use our free Epoch Converter directly online.

Open Epoch Converter →

Sven Schuchardt

Management Consulting · Enterprise Architecture

Bridging the gap between business need and IT & Architecture enablers. With a background in management consulting and enterprise architecture, translating complex technology decisions into clear, actionable insights — written for every stakeholder, from the boardroom to the engineering team.

Connect on LinkedIn