Converting timestamps in Python
Python represents a Unix timestamp — the number of seconds elapsed since
1970-01-01T00:00:00Z — as a plain float or int, and gives you
two different toolkits for working with one. The low-level time module
talks almost exclusively in epoch seconds, while the higher-level datetime
module represents an instant as a datetime object with year, month, day,
hour, minute and second fields. Converting between the two is one of the most common
small tasks in backend code: an API returns created_at as an epoch integer,
a database column stores updated_at as a datetime, and a log
line wants both a human-readable stamp and a sortable numeric one. The conversion itself
is simple — time.time() or datetime.now().timestamp() to go
forward, datetime.fromtimestamp() to go back — but it hides two traps that
catch even experienced Python developers: naive versus timezone-aware
datetime objects, and the difference between a value's local interpretation
and its UTC interpretation. Get either wrong and a timestamp that looks correct in a REPL
will render several hours off once it reaches a server in a different timezone. This page
works through the calls you actually need, with runnable code for each direction and for
the two gotchas that most often turn "just convert this number" into a debugging session
before it does.
Get the current Unix timestamp
time.time() returns the current epoch value directly, as a float. There is
no timezone to get wrong here — it is just a count of seconds since 1970 UTC.
import time
epoch = time.time() # 1786795200.123456 (float, seconds)
epoch_int = int(time.time()) # 1786795200 (whole seconds)
Convert a datetime to a Unix timestamp
Call .timestamp() on a datetime object. This is where the
naive-vs-aware distinction first bites: a naive datetime (no tzinfo) is
assumed to be in your machine's local timezone when .timestamp() runs.
from datetime import datetime, timezone
# Aware datetime — unambiguous, converts correctly anywhere
dt = datetime(2026, 8, 18, 12, 0, 0, tzinfo=timezone.utc)
epoch = dt.timestamp() # 1786795200.0
# Naive datetime — Python assumes LOCAL time, not UTC
naive = datetime(2026, 8, 18, 12, 0, 0)
epoch = naive.timestamp() # differs by your UTC offset
Convert a Unix timestamp to a datetime
Call datetime.fromtimestamp(), and pass tz=timezone.utc
explicitly unless you actually want the result in your machine's local timezone.
from datetime import datetime, timezone
epoch = 1786795200
# Timezone-aware, in UTC — reproducible on any machine
dt_utc = datetime.fromtimestamp(epoch, tz=timezone.utc)
print(dt_utc) # 2026-08-15 04:00:00+00:00
# Naive, in the machine's LOCAL timezone — NOT portable
dt_local = datetime.fromtimestamp(epoch)
For a whole column rather than one value, pandas.to_datetime() is the call —
and its unit argument is where the same seconds-versus-milliseconds bug
lives. It defaults to unit='ns', so a column of ten-digit epoch seconds
handed over unmarked is read as nanoseconds and lands in 1970 with no error raised:
import pandas as pd
s = pd.Series([1786795200, 1786795260])
pd.to_datetime(s, unit='s') # 2026-08-15 04:00:00 — correct
pd.to_datetime(s, unit='ms') # 1970-01-21 — a millisecond column's setting
pd.to_datetime(s) # 1970-01-01 00:00:01.786795200 — the default, in ns
# Aware, and the reverse direction
dt = pd.to_datetime(s, unit='s', utc=True)
dt.astype('int64') // 10**9 # the column back to epoch seconds
Format a datetime with strftime
datetime.fromtimestamp() hands you an object;
.strftime() turns that object into whatever string a log line, a filename
or a CSV column actually needs. It is a method on the datetime rather than a
module-level function — SQL's strftime('%s', …) is a different call
that happens to share the name, and it goes towards an epoch value rather than
away from one. Format an aware datetime and the %z and %Z
codes fill in; format a naive one and both come out empty, which is the quickest way to
spot a datetime that lost its timezone somewhere upstream.
from datetime import datetime, timezone
dt = datetime.fromtimestamp(1786795200, tz=timezone.utc)
dt.strftime('%Y-%m-%d %H:%M:%S') # 2026-08-15 04:00:00
dt.strftime('%Y-%m-%dT%H:%M:%S%z') # 2026-08-15T04:00:00+0000
dt.strftime('%d %b %Y, %I:%M %p %Z') # 15 Aug 2026, 04:00 AM UTC
# The other direction: a formatted string back to epoch seconds
parsed = datetime.strptime('2026-08-15 04:00:00', '%Y-%m-%d %H:%M:%S')
epoch = parsed.replace(tzinfo=timezone.utc).timestamp() # 1786795200.0
Handling timezones
For a specific named timezone rather than UTC or the system default, use
zoneinfo (standard library since Python 3.9). Avoid
datetime.utcfromtimestamp() and datetime.utcnow() — both are
deprecated as of Python 3.12 because they return a naive datetime that merely
contains UTC values, which silently reintroduces the local-time bug the moment it is
compared against an aware datetime. If you just want to see what one epoch value looks
like in a given zone before writing the zoneinfo call,
the UTC-to-local timezone converter renders it with the
DST rules for that exact date already applied.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
epoch = 1786795200
utc_dt = datetime.fromtimestamp(epoch, tz=timezone.utc)
ny_dt = utc_dt.astimezone(ZoneInfo("America/New_York"))
print(ny_dt) # same instant, New York wall-clock time
Common gotchas
-
Naive vs aware. A naive
datetimehas notzinfoand Python silently treats it as local time in most conversions. Attachtzinfo=timezone.utc(or usezoneinfo) the moment a value enters your code, and keep it aware all the way through. -
Seconds vs milliseconds.
datetime.fromtimestamp()always expects seconds. A millisecond value from a JavaScript API, whereDate.now()counts milliseconds, needsepoch_ms / 1000first, or you land in the year 58,391 instead of 2026. -
utcnow()is deprecated. It returns a naive datetime that holds UTC values but does not say so — usedatetime.now(timezone.utc)instead. -
Comparing naive and aware raises
TypeError. Python refuses to compare a naive and an awaredatetimerather than guessing which timezone the naive one is in — pick one representation and stick to it.
Frequently asked questions
How do I get the current Unix timestamp in Python?
Call time.time() from the standard library time module. It
returns the current Unix timestamp as a float, in seconds since 1970-01-01T00:00:00Z. If
you need it as a whole number, wrap it in int(time.time()).
How do I convert a Python datetime to a Unix timestamp?
Call .timestamp() on the datetime object: epoch = dt.timestamp().
If the datetime is naive (no tzinfo), Python assumes it is in your system's
local timezone when doing this conversion, which is rarely what you want for data that
crosses machines — attach tzinfo=timezone.utc first if the datetime
represents UTC.
How do I convert a Unix timestamp to a Python datetime?
Call datetime.fromtimestamp(epoch, tz=timezone.utc) to get a timezone-aware
datetime in UTC. Omitting the tz argument returns a naive datetime in your
system's local timezone instead, which is the single most common source of
off-by-several-hours bugs in Python timestamp code.
Why does datetime.fromtimestamp() give the wrong time?
Almost always because tz was omitted. Without tz=timezone.utc,
fromtimestamp() converts the epoch value into your machine's local timezone,
not UTC. The same epoch number will print a different wall-clock time on a server in a
different timezone unless you pin tz explicitly.
What is the difference between time.time() and datetime.now()?
time.time() always returns a float epoch value in UTC seconds — there is no
timezone ambiguity because it is just a number. datetime.now() returns a
naive datetime in local time by default; pass datetime.now(timezone.utc) to
get an aware datetime in UTC instead.
Where to go next
The same round trip in another language reads a little differently:
converting epoch values in JavaScript works in
milliseconds rather than seconds, and
the SQL epoch functions —
FROM_UNIXTIME(), to_timestamp(),
strftime('%s', …) — do it in the query rather than in Python. If the
number you are about to pass to fromtimestamp() came from somewhere you do
not control, check whether it is seconds or
milliseconds first.