Bug waktu jarang terlihat saat development, lalu meledak di produksi: notifikasi terkirim pukul 3 pagi, laporan bulanan terhitung dua kali, atau pengingat tidak pernah berbunyi. Penyebabnya hampir selalu sama — zona waktu yang tidak eksplisit, input yang tidak divalidasi, dan tepi daylight saving time yang diabaikan. Chapter penutup section ini merangkum materi sebelumnya menjadi daftar periksa praktis. Enam aturan inilah yang akan kita turunkan satu per satu:
- Simpan UTC, tampilkan sesuai zona pembaca.
- Selalu bawa
tzinfo— jangan biarkan objectdatetimenaive melintasi batas sistem. - Validasi setiap parsing teks dari luar dengan
try/except ValueError. - Waspadai tepi DST: satu jam dinding bisa terjadi dua kali.
- Gunakan timestamp Unix untuk menyimpan dan membandingkan momen.
- Injeksi sumber jam saat menguji, dan
time.monotonic()untuk mengukur durasi.
Simpan UTC, Tampilkan Zona Lokal #
UTC berperan sebagai bahasa bersama: server di Singapura, developer di Jakarta, dan database di Frankfurt memahami satu titik referensi yang sama. Konversi ke zona lokal hanya dilakukan di lapisan tampilan:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
wib = ZoneInfo("Asia/Jakarta")
# 1. tempel zona secara eksplisit pada waktu input pengguna
waktu_lokal = datetime(2025, 6, 15, 10, 30).replace(tzinfo=wib)
# 2. simpan sebagai UTC
disimpan = waktu_lokal.astimezone(timezone.utc)
print(disimpan.isoformat())
# 3. tampilkan kembali sesuai zona pembaca
print(disimpan.astimezone(wib).isoformat())Output yang Dihasilkan:
2025-06-15T03:30:00+00:00
2025-06-15T10:30:00+07:00Mari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 7 (
waktu_lokal = ...replace(tzinfo=wib)): input pengguna 10:30 diasumsikan ditulis dalam zona Jakarta, lalu diberi label zona tersebut lewatreplace(). - Baris 10 (
disimpan = ...astimezone(timezone.utc)): momen absolut dikonversi menjadi03:30+00:00— bentuk inilah yang disimpan ke database. - Baris 11 (
print(disimpan.isoformat())): mencetak nilai bentuk simpan berikut offset+00:00. - Baris 14 (
disimpan.astimezone(wib)): saat ditampilkan kembali, nilai UTC dihitung ulang menjadi10:30+07:00untuk pembaca di Jakarta; pembaca di zona lain menerima angka dindingnya sendiri.
Validasi Input Sebelum Parsing #
Aturan ketiga berbunyi: teks dari luar sistem tidak boleh sampai ke strptime() tanpa pengawalan. Dua jenis kegagalan sudah kita lihat di Chapter Parsing dan Formatting datetime — format tidak cocok dan tanggal mustahil — dan keduanya membangkitkan ValueError yang sama:
from datetime import datetime
def parse_tanggal(teks):
"""Kembalikan datetime bila valid, None bila tidak."""
try:
return datetime.strptime(teks, "%d/%m/%Y")
except ValueError as galat:
print(f"Input ditolak: {galat}")
return None
print(parse_tanggal("20/06/2025"))
print(parse_tanggal("31/02/2025"))Output yang Dihasilkan:
2025-06-20 00:00:00
Input ditolak: day is out of range for month
NoneFunction parse_tanggal mengembalikan object datetime untuk teks sah dan None untuk teks ditolak, sehingga pemanggil cukup memeriksa nilai baliknya alih-alih membiarkan program berhenti di tengah jalan.
Tepi DST: Waktu yang Terjadi Dua Kali #
Pada saat fall back — sudah kita saksikan pada Chapter zoneinfo — satu jam dinding terjadi dua kali. Object datetime menandai kedua kemungkinan itu lewat argumen fold:
from datetime import datetime
from zoneinfo import ZoneInfo
new_york = ZoneInfo("America/New_York")
jam_pertama = datetime(2024, 11, 3, 1, 30, tzinfo=new_york, fold=0)
jam_kedua = datetime(2024, 11, 3, 1, 30, tzinfo=new_york, fold=1)
print(jam_pertama.isoformat())
print(jam_kedua.isoformat())
print(jam_kedua.timestamp() - jam_pertama.timestamp())Output yang Dihasilkan:
2024-11-03T01:30:00-04:00
2024-11-03T01:30:00-05:00
3600.0Dua object menampilkan angka dinding identik (01:30) tetapi menunjuk momen berbeda satu jam — dibuktikan selisih timestamp() sebesar 3600.0 detik. Ketika sebuah kejadian dilaporkan “pukul 1:30 pagi” di zona DST, selalu tanyakan yang mana: fold=0 (kemunculan pertama) atau fold=1 (kemunculan kedua). Untuk menghitung durasi nyata yang melintasi perubahan DST, konversikan dulu ke UTC, tambahkan timedelta, lalu konversikan kembali.
Timestamp Unix versus datetime #
Timestamp Unix adalah jumlah detik sejak 1970-01-01 00:00:00 UTC — satu angka float yang sama di semua zona, ideal untuk disimpan dan dibandingkan. Hubungan dua arah dengan datetime ditangani oleh timestamp() dan fromtimestamp():
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
rapat = datetime(2025, 6, 15, 10, 30, tzinfo=ZoneInfo("Asia/Jakarta"))
stempel = rapat.timestamp()
print(stempel)
kembali = datetime.fromtimestamp(stempel, tz=ZoneInfo("Asia/Jakarta"))
print(kembali.isoformat())
utc = datetime.fromtimestamp(stempel, tz=timezone.utc)
print(utc.isoformat())Output yang Dihasilkan:
1749958200.0
2025-06-15T10:30:00+07:00
2025-06-15T03:30:00+00:00Satu timestamp 1749958200.0 dibaca kembali sebagai 10:30+07:00 di Jakarta dan 03:30+00:00 di UTC — dua tampilan dari momen yang sama. Selalu berikan argumen tz= pada fromtimestamp(); tanpa argumen tersebut, hasilnya berupa object naive yang mengikuti zona waktu sistem operasi dan menjadi ambigu.
Menguji Waktu dan Mengukur Durasi #
Kode yang memanggil datetime.now() langsung tidak dapat diuji dengan pasti — hasilnya berubah setiap eksekusi. Solusinya adalah menyuntikkan sumber jam lewat parameter:
from datetime import datetime, timezone
def sapa_sekarang(now_provider=datetime.now):
now = now_provider(tz=timezone.utc)
if 5 <= now.hour < 12:
return "Selamat pagi"
if 12 <= now.hour < 15:
return "Selamat siang"
if 15 <= now.hour < 19:
return "Selamat sore"
return "Selamat malam"
# produksi: pakai waktu nyata (hasil berubah-ubah)
# print(sapa_sekarang())
# pengujian: pasang jam palsu yang deterministik
waktu_uji = lambda tz=None: datetime(2025, 6, 15, 20, 0, tzinfo=timezone.utc)
print(sapa_sekarang(now_provider=waktu_uji))Output yang Dihasilkan:
Selamat malamNilai bawaan parameter now_provider=datetime.now menjaga perilaku produksi tetap memakai jam nyata, sementara pengujian mengirimkan waktu_uji yang selalu mengembalikan 20:00 UTC — output Selamat malam menjadi deterministik. Untuk mengukur durasi, gunakan time.monotonic() alih-alih time.time():
| Sifat | time.time() | time.monotonic() |
|---|---|---|
| Sumber nilai | jam dinding sistem operasi | penghitung yang selalu bertambah |
| Bisa lompat/mundur? | bisa — sinkronisasi NTP, koreksi admin | tidak pernah |
| Titik nol | epoch Unix 1970 | tidak terdefinisi, tidak boleh diasumsikan |
| Cocok untuk | stempel waktu absolut | mengukur durasi antar dua titik |
import time
a = time.monotonic()
b = time.monotonic()
print(b > a)
mulai = time.monotonic()
time.sleep(0.1)
print(time.monotonic() - mulai >= 0.1)Output yang Dihasilkan:
True
Truetime.monotonic() menjamin nilai berikutnya selalu lebih besar, sehingga durasi hasil pengurangannya tidak pernah menjadi negatif gara-gara jam sistem dikoreksi ke belakang. Dengan enam aturan ini — UTC sebagai penyimpanan, tzinfo yang selalu eksplisit, parsing yang divalidasi, fold pada tepi DST, timestamp untuk momen absolut, serta jam yang diinjeksi dan durasi yang monoton — aplikasi kita siap menghadapi waktu dunia nyata tanpa kecelakaan.