Date & Time #

Tanggal dan waktu terlihat mudah sampai kamu mulai berurusan dengan zona waktu, format yang berbeda-beda, dan kalkulasi durasi yang kompleks. Python menyediakan modul datetime di stdlib yang komprehensif untuk semua kebutuhan ini. Yang paling penting dipahami — dan paling sering salah diimplementasikan — adalah perbedaan antara naive datetime (tanpa informasi zona waktu) dan aware datetime (dengan zona waktu). Menggunakan naive datetime di aplikasi yang melayani pengguna dari banyak zona waktu adalah resep bug yang sulit di-debug. Artikel ini membahas semua aspek pengelolaan waktu di Python secara komprehensif.

Kelas-kelas di Modul datetime #

Modul datetime menyediakan empat kelas utama yang saling terkait:

from datetime import date, time, datetime, timedelta, timezone

# date — hanya tanggal (tahun, bulan, hari)
# time — hanya waktu (jam, menit, detik, microsecond)
# datetime — gabungan tanggal + waktu
# timedelta — durasi / selisih waktu
# timezone — representasi zona waktu sederhana (offset tetap)
Hubungan antar kelas:

date(2024, 3, 15)       → hanya tanggal
time(14, 30, 0)         → hanya waktu
datetime(2024, 3, 15,
         14, 30, 0)     → tanggal + waktu

datetime = date + time  (digabungkan)
datetime - datetime     = timedelta
datetime + timedelta    = datetime

date — Hanya Tanggal #

from datetime import date

# Membuat objek date
hari_ini = date.today()
print(hari_ini)          # → 2024-03-15

tanggal_spesifik = date(2024, 8, 17)   # (tahun, bulan, hari)
print(tanggal_spesifik)  # → 2024-08-17

# Atribut komponen
print(hari_ini.year)    # → 2024
print(hari_ini.month)   # → 3
print(hari_ini.day)     # → 15

# Informasi hari
print(hari_ini.weekday())       # → 0–6, Senin=0, Minggu=6
print(hari_ini.isoweekday())    # → 1–7, Senin=1, Minggu=7
print(hari_ini.strftime("%A"))  # → nama hari dalam bahasa sistem

# Konversi
print(hari_ini.isoformat())     # → "2024-03-15"
print(hari_ini.timetuple())     # → time.struct_time(...)

# Membuat date dari ISO string
dari_iso = date.fromisoformat("2024-08-17")
print(dari_iso)   # → 2024-08-17

# Membuat date dari ordinal (jumlah hari sejak 1 Januari 0001)
dari_ordinal = date.fromordinal(738969)
print(dari_ordinal)

datetime — Tanggal dan Waktu #

from datetime import datetime

# Waktu saat ini (naive — tanpa timezone)
sekarang = datetime.now()
print(sekarang)          # → 2024-03-15 14:30:45.123456

# Membuat datetime spesifik
dt = datetime(2024, 8, 17, 9, 0, 0)   # (tahun, bulan, hari, jam, menit, detik)
print(dt)   # → 2024-08-17 09:00:00

# Atribut komponen
print(sekarang.year)        # → 2024
print(sekarang.month)       # → 3
print(sekarang.day)         # → 15
print(sekarang.hour)        # → 14
print(sekarang.minute)      # → 30
print(sekarang.second)      # → 45
print(sekarang.microsecond) # → 123456
print(sekarang.weekday())   # → 4 (Jumat)

# Ekstrak bagian date dan time
print(sekarang.date())   # → 2024-03-15  (objek date)
print(sekarang.time())   # → 14:30:45.123456  (objek time)

# Ganti komponen tertentu (membuat objek baru)
dt_baru = sekarang.replace(hour=0, minute=0, second=0, microsecond=0)
print(dt_baru)   # → 2024-03-15 00:00:00

# Gabungkan date dan time
tgl = date(2024, 8, 17)
wkt = time(9, 30, 0)
dt_gabung = datetime.combine(tgl, wkt)
print(dt_gabung)   # → 2024-08-17 09:30:00

timedelta — Durasi dan Selisih Waktu #

timedelta mewakili durasi — dapat digunakan untuk operasi aritmatika pada objek date dan datetime:

from datetime import datetime, timedelta

sekarang = datetime.now()

# Membuat timedelta
satu_hari   = timedelta(days=1)
satu_jam    = timedelta(hours=1)
satu_minggu = timedelta(weeks=1)
campuran    = timedelta(days=2, hours=3, minutes=30, seconds=15)

# Aritmatika
besok     = sekarang + timedelta(days=1)
kemarin   = sekarang - timedelta(days=1)
dua_jam_lagi = sekarang + timedelta(hours=2)

print(besok.date())      # → 2024-03-16
print(kemarin.date())    # → 2024-03-14

# Selisih antara dua datetime menghasilkan timedelta
mulai = datetime(2024, 1, 1)
selesai = datetime(2024, 3, 15, 14, 30)
durasi = selesai - mulai

print(durasi)               # → 74 days, 14:30:00
print(durasi.days)          # → 74
print(durasi.seconds)       # → 52200 (detik dalam hari terakhir, bukan total!)
print(durasi.total_seconds()) # → 6442200.0  ← gunakan ini untuk total detik

# Konversi durasi ke jam/menit
total_detik = int(durasi.total_seconds())
jam   = total_detik // 3600
menit = (total_detik % 3600) // 60
print(f"{jam} jam {menit} menit")   # → 1789 jam 30 menit
# ANTI-PATTERN: menggunakan .seconds untuk total durasi
delta = timedelta(days=2, hours=5)
print(delta.seconds)          # → 18000 (hanya detik dalam hari terakhir!)
print(delta.total_seconds())  # → 190800.0  ← ini yang benar untuk total

# BENAR: selalu gunakan total_seconds() jika butuh total durasi dalam detik

Parsing dan Formatting #

strptime — Parse String ke datetime #

from datetime import datetime

# strptime(string, format) — parse string ke datetime
dt1 = datetime.strptime("2024-08-17", "%Y-%m-%d")
dt2 = datetime.strptime("17/08/2024 09:30", "%d/%m/%Y %H:%M")
dt3 = datetime.strptime("17 Agustus 2024", "%d %B %Y")
dt4 = datetime.strptime("2024-08-17T09:30:00", "%Y-%m-%dT%H:%M:%S")

print(dt1)   # → 2024-08-17 00:00:00
print(dt2)   # → 2024-08-17 09:30:00

strftime — Format datetime ke String #

dt = datetime(2024, 8, 17, 9, 30, 45)

print(dt.strftime("%Y-%m-%d"))             # → 2024-08-17
print(dt.strftime("%d/%m/%Y"))             # → 17/08/2024
print(dt.strftime("%d %B %Y"))             # → 17 August 2024
print(dt.strftime("%H:%M:%S"))             # → 09:30:45
print(dt.strftime("%Y-%m-%dT%H:%M:%S"))   # → 2024-08-17T09:30:45
print(dt.strftime("%A, %d %B %Y"))         # → Saturday, 17 August 2024
print(dt.strftime("%I:%M %p"))             # → 09:30 AM  (12-jam)

Tabel Kode Format Penting #

Kode  │ Keterangan              │ Contoh
──────┼─────────────────────────┼──────────────
%Y    │ Tahun 4 digit           │ 2024
%y    │ Tahun 2 digit           │ 24
%m    │ Bulan (01–12)           │ 08
%B    │ Nama bulan penuh        │ August
%b    │ Nama bulan singkat      │ Aug
%d    │ Hari (01–31)            │ 17
%A    │ Nama hari penuh         │ Saturday
%a    │ Nama hari singkat       │ Sat
%H    │ Jam 24-jam (00–23)      │ 09
%I    │ Jam 12-jam (01–12)      │ 09
%M    │ Menit (00–59)           │ 30
%S    │ Detik (00–59)           │ 45
%f    │ Microsecond (000000)    │ 123456
%p    │ AM/PM                   │ AM
%Z    │ Nama timezone           │ WIB
%z    │ Offset UTC (+HHMM)      │ +0700
%j    │ Hari dalam setahun      │ 230
%W    │ Nomor minggu (Sen=awal) │ 33

ISO 8601 — Format Standar Internasional #

from datetime import datetime, timezone

# isoformat() — hasilkan string ISO 8601
dt = datetime(2024, 8, 17, 9, 30, 45)
print(dt.isoformat())              # → 2024-08-17T09:30:45
print(dt.isoformat(sep=" "))       # → 2024-08-17 09:30:45
print(dt.isoformat(timespec="minutes"))  # → 2024-08-17T09:30

# fromisoformat() — parse string ISO 8601 (Python 3.7+)
dt = datetime.fromisoformat("2024-08-17T09:30:45")
print(dt)   # → 2024-08-17 09:30:45

# Python 3.11+ mendukung format ISO yang lebih lengkap termasuk Z
# dt = datetime.fromisoformat("2024-08-17T09:30:45Z")

Unix Timestamp #

Unix timestamp adalah jumlah detik sejak 1 Januari 1970 00:00:00 UTC — format umum untuk menyimpan waktu di database dan API:

from datetime import datetime, timezone
import time as time_module

# Dapatkan timestamp saat ini
ts_sekarang = time_module.time()
print(ts_sekarang)   # → 1710506445.123456

# Konversi datetime ke timestamp
dt = datetime(2024, 8, 17, 9, 0, 0, tzinfo=timezone.utc)
ts = dt.timestamp()
print(ts)   # → 1723885200.0

# Konversi timestamp ke datetime (local time)
dt_lokal = datetime.fromtimestamp(ts)
print(dt_lokal)

# Konversi timestamp ke datetime UTC
dt_utc = datetime.fromtimestamp(ts, tz=timezone.utc)
print(dt_utc)   # → 2024-08-17 09:00:00+00:00

Naive vs Aware Datetime #

Ini adalah konsep paling penting dan paling sering salah diimplementasikan saat bekerja dengan waktu:

Naive datetime  → tidak tahu zona waktu → bahaya di aplikasi multi-timezone
Aware datetime  → tahu zona waktunya → selalu gunakan ini untuk menyimpan waktu
from datetime import datetime, timezone

# Naive — tidak ada info timezone
naive = datetime.now()
print(naive.tzinfo)   # → None

# Aware — ada info timezone
aware_utc = datetime.now(tz=timezone.utc)
print(aware_utc.tzinfo)   # → UTC

# ANTI-PATTERN: menyimpan waktu sebagai naive dan berharap semuanya UTC
waktu_dibuat = datetime.now()            # naive — zona waktu server mana?
waktu_diperbarui = datetime.utcnow()    # masih naive! meski secara nilai UTC

# BENAR: selalu gunakan aware datetime dengan UTC untuk penyimpanan
waktu_dibuat = datetime.now(tz=timezone.utc)       # aware UTC
waktu_diperbarui = datetime.now(tz=timezone.utc)   # aware UTC
datetime.utcnow() mengembalikan naive datetime yang secara nilai adalah UTC — tapi Python tidak tahu itu UTC. Ini adalah sumber bug timezone klasik. Gunakan datetime.now(tz=timezone.utc) yang mengembalikan aware datetime dan tidak ambigu. utcnow() sudah deprecated sejak Python 3.12.

Zona Waktu dengan zoneinfo (Python 3.9+) #

zoneinfo adalah modul stdlib baru yang menggantikan pytz untuk sebagian besar kasus:

from datetime import datetime
from zoneinfo import ZoneInfo

# Buat aware datetime dengan zona waktu tertentu
wib = ZoneInfo("Asia/Jakarta")       # UTC+7
wita = ZoneInfo("Asia/Makassar")     # UTC+8
wit = ZoneInfo("Asia/Jayapura")      # UTC+9
utc = ZoneInfo("UTC")

# Waktu saat ini di zona tertentu
sekarang_wib = datetime.now(tz=wib)
print(sekarang_wib)   # → 2024-03-15 14:30:45.123456+07:00

# Konversi antara zona waktu
sekarang_utc = datetime.now(tz=utc)
sekarang_wib  = sekarang_utc.astimezone(wib)
sekarang_wita = sekarang_utc.astimezone(wita)
sekarang_wit  = sekarang_utc.astimezone(wit)

print(sekarang_utc.strftime("%H:%M %Z"))    # → 07:30 UTC
print(sekarang_wib.strftime("%H:%M %Z"))    # → 14:30 WIB
print(sekarang_wita.strftime("%H:%M %Z"))   # → 15:30 WITA
print(sekarang_wit.strftime("%H:%M %Z"))    # → 16:30 WIT
# Daftar semua zona waktu yang tersedia
import zoneinfo
zona_asia = sorted(z for z in zoneinfo.available_timezones() if z.startswith("Asia/"))
print(zona_asia[:5])   # → ['Asia/Aden', 'Asia/Almaty', 'Asia/Amman', ...]

pytz — Untuk Kompatibilitas Python < 3.9 #

# Jika perlu mendukung Python < 3.9, gunakan pytz
# pip install pytz

import pytz
from datetime import datetime

wib = pytz.timezone("Asia/Jakarta")

# ANTI-PATTERN dengan pytz: jangan langsung replace tzinfo
naive = datetime(2024, 8, 17, 9, 0, 0)
# salah = naive.replace(tzinfo=wib)  # ← hasilnya SALAH untuk timezone dengan DST historis

# BENAR dengan pytz: selalu gunakan localize() untuk naive → aware
aware = wib.localize(naive)
print(aware)   # → 2024-08-17 09:00:00+07:00

# Konversi zona
utc = pytz.utc
aware_utc = aware.astimezone(utc)
print(aware_utc)   # → 2024-08-17 02:00:00+00:00

Perbandingan dan Pengurutan Datetime #

from datetime import datetime, timezone

dt1 = datetime(2024, 1, 1)
dt2 = datetime(2024, 6, 15)
dt3 = datetime(2024, 1, 1)

# Perbandingan
print(dt1 < dt2)    # → True
print(dt1 == dt3)   # → True
print(dt2 > dt1)    # → True

# Cari minimum dan maksimum
tanggal_list = [datetime(2024, 3, 15), datetime(2024, 1, 1), datetime(2024, 12, 25)]
print(min(tanggal_list))   # → 2024-01-01 00:00:00
print(max(tanggal_list))   # → 2024-12-25 00:00:00

# Urutkan list datetime
terurut = sorted(tanggal_list)
print(terurut)

# JANGAN bandingkan naive dan aware — akan TypeError
naive = datetime(2024, 1, 1)
aware = datetime(2024, 1, 1, tzinfo=timezone.utc)
# naive < aware  # → TypeError: can't compare offset-naive and offset-aware datetimes

Kalkulasi Praktis #

from datetime import datetime, timedelta, date
from zoneinfo import ZoneInfo

wib = ZoneInfo("Asia/Jakarta")

# Awal dan akhir hari
def awal_hari(dt: datetime) -> datetime:
    return dt.replace(hour=0, minute=0, second=0, microsecond=0)

def akhir_hari(dt: datetime) -> datetime:
    return dt.replace(hour=23, minute=59, second=59, microsecond=999999)

sekarang = datetime.now(tz=wib)
print(awal_hari(sekarang))   # → 2024-03-15 00:00:00+07:00
print(akhir_hari(sekarang))  # → 2024-03-15 23:59:59.999999+07:00

# Awal bulan dan akhir bulan
def awal_bulan(dt: datetime) -> datetime:
    return dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)

def akhir_bulan(dt: datetime) -> datetime:
    import calendar
    hari_terakhir = calendar.monthrange(dt.year, dt.month)[1]
    return dt.replace(day=hari_terakhir, hour=23, minute=59, second=59, microsecond=999999)

# Hitung umur
def hitung_umur(tanggal_lahir: date) -> int:
    hari_ini = date.today()
    umur = hari_ini.year - tanggal_lahir.year
    # Kurangi 1 jika ulang tahun belum lewat tahun ini
    if (hari_ini.month, hari_ini.day) < (tanggal_lahir.month, tanggal_lahir.day):
        umur -= 1
    return umur

lahir = date(1995, 8, 17)
print(f"Umur: {hitung_umur(lahir)} tahun")

# Hitung hari kerja antara dua tanggal (tanpa library tambahan)
def hitung_hari_kerja(mulai: date, selesai: date) -> int:
    """Hitung jumlah hari kerja (Senin–Jumat) antara dua tanggal."""
    total = 0
    tanggal = mulai
    while tanggal <= selesai:
        if tanggal.weekday() < 5:  # 0=Senin, 4=Jumat
            total += 1
        tanggal += timedelta(days=1)
    return total

mulai = date(2024, 3, 1)
selesai = date(2024, 3, 31)
print(f"Hari kerja Maret 2024: {hitung_hari_kerja(mulai, selesai)}")

Mengukur Waktu Eksekusi dengan time #

Modul time (berbeda dari datetime.time) berguna untuk mengukur performa kode:

import time

# time.time() — detik sejak epoch (float)
mulai = time.time()
# ... operasi yang diukur ...
selesai = time.time()
print(f"Durasi: {selesai - mulai:.4f} detik")

# time.perf_counter() — presisi tinggi untuk benchmarking (direkomendasikan)
mulai = time.perf_counter()
sum(i**2 for i in range(1_000_000))
selesai = time.perf_counter()
print(f"Durasi: {selesai - mulai:.6f} detik")

# time.sleep() — jeda eksekusi
print("Mulai...")
time.sleep(2)   # jeda 2 detik
print("Selesai setelah 2 detik")

# timeit — mengukur rata-rata dari banyak eksekusi
import timeit
durasi = timeit.timeit(
    stmt='"-".join(str(n) for n in range(100))',
    number=10_000
)
print(f"Rata-rata: {durasi/10_000*1_000:.4f} ms")

Ringkasan #

  • Selalu gunakan aware datetime untuk menyimpan dan membandingkan waktu di aplikasi nyata. Naive datetime tanpa timezone adalah sumber bug yang sulit di-debug.
  • Hindari datetime.utcnow() — sudah deprecated di Python 3.12. Gunakan datetime.now(tz=timezone.utc) yang mengembalikan aware datetime.
  • zoneinfo (Python 3.9+) adalah cara modern menangani timezone — sudah built-in, tidak perlu install pytz. Gunakan ZoneInfo("Asia/Jakarta") untuk WIB.
  • Jika pakai pytz, gunakan tz.localize(naive_dt) bukan naive_dt.replace(tzinfo=tz) — hasilnya bisa salah untuk timezone dengan sejarah DST.
  • timedelta.seconds hanya mengembalikan detik dalam hari terakhir, bukan total. Gunakan timedelta.total_seconds() untuk total durasi dalam detik.
  • ISO 8601 (%Y-%m-%dT%H:%M:%S) adalah format pertukaran data yang paling direkomendasikan — gunakan dt.isoformat() dan datetime.fromisoformat().
  • Jangan bandingkan naive dan aware datetime — Python akan melempar TypeError. Pastikan keduanya dalam status yang sama sebelum dibandingkan.
  • time.perf_counter() untuk benchmarking kode — lebih presisi dari time.time() yang dipengaruhi perubahan jam sistem.

← Sebelumnya: Dictionary   Berikutnya: Regex →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact