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:

  1. Simpan UTC, tampilkan sesuai zona pembaca.
  2. Selalu bawa tzinfo — jangan biarkan object datetime naive melintasi batas sistem.
  3. Validasi setiap parsing teks dari luar dengan try/except ValueError.
  4. Waspadai tepi DST: satu jam dinding bisa terjadi dua kali.
  5. Gunakan timestamp Unix untuk menyimpan dan membandingkan momen.
  6. 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:

python
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:

text
2025-06-15T03:30:00+00:00
2025-06-15T10:30:00+07:00

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 7 (waktu_lokal = ...replace(tzinfo=wib)): input pengguna 10:30 diasumsikan ditulis dalam zona Jakarta, lalu diberi label zona tersebut lewat replace().
  2. Baris 10 (disimpan = ...astimezone(timezone.utc)): momen absolut dikonversi menjadi 03:30+00:00 — bentuk inilah yang disimpan ke database.
  3. Baris 11 (print(disimpan.isoformat())): mencetak nilai bentuk simpan berikut offset +00:00.
  4. Baris 14 (disimpan.astimezone(wib)): saat ditampilkan kembali, nilai UTC dihitung ulang menjadi 10:30+07:00 untuk 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:

python
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:

text
2025-06-20 00:00:00
Input ditolak: day is out of range for month
None

Function 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:

python
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:

text
2024-11-03T01:30:00-04:00
2024-11-03T01:30:00-05:00
3600.0

Dua 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():

python
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:

text
1749958200.0
2025-06-15T10:30:00+07:00
2025-06-15T03:30:00+00:00

Satu 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:

python
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:

text
Selamat malam

Nilai 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():

Sifattime.time()time.monotonic()
Sumber nilaijam dinding sistem operasipenghitung yang selalu bertambah
Bisa lompat/mundur?bisa — sinkronisasi NTP, koreksi admintidak pernah
Titik nolepoch Unix 1970tidak terdefinisi, tidak boleh diasumsikan
Cocok untukstempel waktu absolutmengukur durasi antar dua titik
python
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:

text
True
True

time.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.