Server kamu menampilkan waktu yang mundur tujuh jam, padahal kode terlihat benar. Lalu ada pengguna yang lapor tanggal lahirnya berubah jadi kemarin. Hampir semua bug semacam ini berakar dari satu hal: mencampur datetime yang naive dengan yang aware. Kabar baiknya, kamu tidak butuh library pihak ketiga untuk beres. Modul datetime bawaan Python sudah cukup, asal kamu paham tiga objek utamanya: datetime, timedelta, dan timezone.

Objek Dasar: datetime, date, dan time

Modul datetime punya beberapa tipe. Yang paling sering kamu pakai adalah datetime (tanggal plus jam), date (hanya tanggal), time (hanya jam), dan timedelta (selisih waktu). Mulai dengan impor dan lihat nilai now():

from datetime import datetime, date

sekarang = datetime.now()
print(sekarang)
# 2026-10-05 14:30:12.456789

print(date.today())
# 2026-10-05

datetime.now() mengembalikan waktu lokal mesin tanpa informasi zona waktu. Objek seperti ini disebut naive. Ini bukan masalah sampai kamu mulai membandingkan atau menyimpan waktu dari sistem lain, yang akan kita bahas di bagian timezone.

Membuat Objek datetime Secara Manual

Kalau kamu sudah punya komponen tanggal terpisah (tahun, bulan, hari, jam), buat objeknya langsung lewat konstruktor:

python
from datetime import datetime

jadwal = datetime(2026, 10, 5, 14, 30, 0)
print(jadwal)
# 2026-10-05 14:30:00

Argumennya urut: tahun, bulan, hari, jam, menit, detik, mikrodetik. Yang wajib hanya tiga pertama, sisanya default nol. Tipe date dan time juga punya konstruktor serupa: date(2026, 10, 5) dan time(14, 30).

Mengubah String Menjadi datetime (strptime)

Data tanggal di dunia nyata hampir selalu datang sebagai string: dari API, form HTML, atau file CSV. Kamu butuh dua fungsi untuk bolak-balik. strptime() mengurai string menjadi objek datetime berdasarkan pola yang kamu tentukan:

python
from datetime import datetime

mentah = "05-10-2026 14:30"
dt = datetime.strptime(mentah, "%d-%m-%Y %H:%M")
print(dt)
# 2026-10-05 14:30:00

Direktif %d berarti hari, %m bulan, %Y tahun empat digit, %H jam format 24 jam, dan %M menit. Salah satu direktif saja meleset, strptime melempar ValueError. Untuk string berformat ISO 8601 (standar JSON dan kebanyakan API), ada jalan pintas datetime.fromisoformat() yang tidak butuh pola manual:

python
dt = datetime.fromisoformat("2026-10-05T14:30:00+07:00")
print(dt)
# 2026-10-05 14:30:00+07:00

Simpan dan kirim tanggal antar sistem dalam format ISO 8601 (YYYY-MM-DDTHH:MM:SS+HH:MM). Format ini bisa diurai ulang oleh hampir semua bahasa, dan tidak ambigu seperti 05/10/2026 yang bisa berarti 5 Oktober atau 10 Mei tergantung negaranya.

Format datetime Menjadi String (strftime)

Arah sebaliknya pakai strftime(). Berguna saat menampilkan tanggal ke pengguna atau membuat nama file:

python
dt = datetime(2026, 10, 5, 14, 30)

print(dt.strftime("%d %B %Y"))
# 05 October 2026

print(dt.strftime("%A, %d/%m/%Y %H:%M"))
# Monday, 05/10/2026 14:30

%B nama bulan penuh, %A nama hari penuh. Output keduanya mengikuti locale sistem. Konsistensi antar server lebih penting daripada nama bulan lokal, jadi jangan bergantung padanya.

Menghitung Selisih Waktu dengan timedelta

timedelta mewakili selisih antara dua titik waktu, dan kamu bisa menjumlahkan atau mengurangkannya dengan datetime:

python
from datetime import datetime, timedelta

sekarang = datetime(2026, 10, 5, 14, 30)

besok = sekarang + timedelta(days=1)
print(besok)  # 2026-10-06 14:30:00

seminggu_lalu = sekarang - timedelta(weeks=1)
print(seminggu_lalu)  # 2026-09-28 14:30:00

selisih = besok - sekarang
print(selisih.days)  # 1
print(selisih.total_seconds())  # 86400.0

Kata kunci timedelta yang tersedia: weeks, days, hours, minutes, seconds, milliseconds, dan microseconds. Tidak ada months atau years, karena panjang bulan tidak tetap. Untuk menambah satu bulan, kamu harus menghitung manual atau pakai pustaka seperti dateutil.

Timezone: Aware vs Naive

Ini bagian yang paling sering bikin bug. Objek datetime ada dua macam: naive (tanpa zona waktu) dan aware (membawa zona waktu). Python menolak membandingkan keduanya, dan itulah sumber TypeError yang membingungkan banyak orang:

python
from datetime import datetime, timezone

naive = datetime(2026, 10, 5, 14, 30)
aware = datetime(2026, 10, 5, 14, 30, tzinfo=timezone.utc)

print(naive == aware)
# TypeError: can't compare offset-naive and offset-aware datetimes

Jangan diam-diam menganggap semua datetime kamu UTC hanya karena server berada di UTC. Objek datetime.now() tetap naive meski mesinnya memakai UTC. Selalu set zona waktu secara eksplisit.

Untuk menambahkan zona waktu, gunakan zoneinfo (standar sejak Python 3.9), pengganti pytz yang dulu populer. Contoh mengubah waktu Jakarta menjadi UTC:

python
from datetime import datetime
from zoneinfo import ZoneInfo

jakarta = datetime(2026, 10, 5, 14, 30, tzinfo=ZoneInfo("Asia/Jakarta"))
print(jakarta)  # 2026-10-05 14:30:00+07:00

utc = jakarta.astimezone(ZoneInfo("UTC"))
print(utc)  # 2026-10-05 07:30:00+00:00

astimezone() mengubah representasi waktu ke zona lain tanpa mengubah momen yang sebenarnya. Jam bergeser dari 14:30 menjadi 07:30 karena Jakarta tujuh jam lebih cepat dari UTC, tapi keduanya menunjuk momen yang sama. Untuk aplikasi server, aturan umumnya: simpan semua waktu dalam UTC, lalu konversi ke zona lokal pengguna hanya saat ditampilkan.

Bekerja dengan Timestamp Epoch

Banyak sistem (database, log, API) menyimpan waktu sebagai timestamp epoch: jumlah detik sejak 1 Januari 1970 UTC. time.time() mengembalikan nilai itu untuk momen sekarang; contoh di bawah memakai nilai tetap supaya hasilnya deterministik:

python
from datetime import datetime, timezone

ts = 1791185412.123
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
print(dt)  # 2026-10-05 07:30:12.123000+00:00

print(dt.timestamp())  # 1791185412.123

Selalu beri tz=timezone.utc pada fromtimestamp(). Tanpa itu, Python mengasumsikan waktu lokal mesin, dan hasilnya bisa bergeser saat server kamu pindah ke zona lain.

Menyimpan dan Memvalidasi datetime di Aplikasi Nyata

Begitu kode tanggalmu beres, dua hal lain biasanya menyusul: validasi input dan penyimpanan ke database. Kalau kamu memvalidasi payload API, Pydantic punya tipe datetime yang otomatis mengurai string ISO 8601 dan menolak format yang salah. Untuk menyimpannya, SQLAlchemy menyediakan kolom DateTime dengan opsi timezone=True. Kalau kamu menganalisis data deret waktu dalam jumlah besar, Pandas punya pd.to_datetime() untuk mengurai ribuan baris tanggal sekaligus, jauh lebih cepat daripada loop strptime.

Aturan yang bisa kamu tempel di dekat monitor: terima input sebagai string, ubah jadi datetime aware secepat mungkin, simpan dalam UTC, dan format ulang hanya untuk tampilan. Kalau semua kode tanggalmu mengikuti alur itu, bug tujuh jam yang bikin server tampak salah jam tidak akan muncul lagi. Coba refactor satu endpoint di proyekmu yang masih memakai datetime.now() polos dan ganti dengan UTC plus astimezone().