8 November 2026•baca 7 menit

Python stdlib: datetime yang Ditolak json.dumps

Skrip yang menjumlahkan menit per hari mati tepat di baris yang menulis laporan. Empat modul stdlib membawanya, dan satu TypeError menghadang.

Baca dalam bahasa Inggris

Kamu mengarahkan skrip ke folder berisi tiga file log. Skrip membacanya, tidak mencetak apa pun, lalu mati dengan TypeError di baris yang menulis laporan. Dua baris terakhir error-nya begini:

TEXT
$ python3 daily.py logs
TypeError: Object of type datetime is not JSON serializable
when serializing dict item 'generated_at'

Baris kedua menyebut key dict yang menyimpan nilai yang ditolak json. Angka-angka di sini berasal dari Python 3.14.9 di Linux.

Poin Penting

  • json.dumps menulis sekumpulan tipe dasar yang tetap, dan datetime bukan salah satunya, jadi encoder melempar error alih-alih memilih format untukmu.
  • datetime.now() mengembalikan waktu lokal mesin tanpa zona waktu, dan isoformat(timespec="seconds") adalah cara resmi di dokumentasi untuk mengubahnya menjadi string yang bisa ditulis json.
  • argparse menjawab -h tanpa diminta, pathlib mengubah folder menjadi path dan teks, dan json mengubah teks menjadi list dan dict.
  • Checker yang hanya memastikan stamp bisa di-parse tidak bisa membedakan timespec="hours" dari timespec="seconds", jadi ia membuktikan bentuknya, bukan presisinya.

Empat modul, satu program kecil

daily.py menjumlahkan menit per hari dari folder berisi log sesi. Masing-masing dari empat modul standard library punya satu tugas.

PYTHON
# daily.py
import argparse
import json
from datetime import datetime
from pathlib import Path

parser = argparse.ArgumentParser(description="Minutes per day from a folder of session logs")
parser.add_argument("folder")
args = parser.parse_args()

total = {}
for path in sorted(Path(args.folder).glob("*.json")):
    if path.name == "summary.json":
        continue
    for session in json.loads(path.read_text()):
        total[session["date"]] = total.get(session["date"], 0) + session["minutes"]

report = {
    "generated_at": datetime.now(),
    "days": [{"date": day, "minutes": minutes} for day, minutes in sorted(total.items())],
}

out = Path(args.folder) / "summary.json"
out.write_text(json.dumps(report, indent=2))
print(f"{len(report['days'])} days from {len(list(Path(args.folder).glob('*.json'))) - 1} files")

argparse membaca satu argumen, yaitu folder. Kamu tidak pernah meminta -h, tapi opsi itu tetap ada:

TEXT
$ python3 daily.py --help
usage: daily.py [-h] folder

Minutes per day from a folder of session logs

positional arguments:
  folder

options:
  -h, --help  show this help message and exit

pathlib membangun path. glob("*.json") mendaftar file log, dan read_text() memberimu tiap file sebagai string. json mengubah teks itu menjadi list dan dict, lalu mengubah laporan kembali menjadi teks. datetime menyediakan timestamp.

Glob itu juga cocok dengan summary.json, file yang akan segera ditulis skrip. if melewatinya. Tanpa itu, run kedua akan membaca outputnya sendiri sebagai log sesi. - 1 di baris terakhir mengeluarkannya dari hitungan file, karena saat itu file tersebut sudah ada.

Kenapa json menolak datetime

Modul json menulis daftar tipe yang pendek: string, number, boolean, None, list, dan dict. datetime tidak ada di daftar itu.

Saat json.dumps bertemu tipe di luar daftar itu, ia melempar TypeError. Ia tidak menebak format. Tanggal bisa ditulis dengan banyak cara, jadi seseorang harus memilih satu. Di sini pilihannya ISO 8601: tahun, bulan, hari, huruf T, lalu waktu. Cara resmi untuk mendapatkan string itu adalah isoformat().

Checker-nya

check_daily.py adalah skrip biasa tanpa framework tes. Jalankan dari folder yang berisi daily.py.

PYTHON
# check_daily.py
import json
import subprocess
import sys
import tempfile
from datetime import datetime
from pathlib import Path

FIXTURE = {
    "mon.json": [{"date": "2026-11-06", "minutes": 25}, {"date": "2026-11-06", "minutes": 35}],
    "tue.json": [{"date": "2026-11-07", "minutes": 40}],
    "wed.json": [{"date": "2026-11-02", "minutes": 15}, {"date": "2026-11-08", "minutes": 50}],
}
EXPECTED = {"2026-11-02": 15, "2026-11-06": 60, "2026-11-07": 40, "2026-11-08": 50}


def report(results):
    for label, ok, detail in results:
        print(f"{'PASS' if ok else 'FAIL'}  {label}" + (f"  <- {detail}" if not ok and detail else ""))
    passed = sum(1 for _, ok, _ in results if ok)
    print(f"\n{passed} of {len(results)} checks passed")
    return passed == len(results)


def main():
    folder = Path(tempfile.mkdtemp())
    for name, sessions in FIXTURE.items():
        (folder / name).write_text(json.dumps(sessions))

    run = subprocess.run([sys.executable, "daily.py", str(folder)], capture_output=True, text=True)
    crash = ""
    for line in reversed(run.stderr.strip().splitlines()):
        if line.split(":")[0].endswith("Error"):
            crash = line
            break
    if run.returncode != 0:
        report([("the script runs without crashing", False, crash)])
        return 1

    results = [("the script runs without crashing", True, "")]
    out = folder / "summary.json"
    results.append(("summary.json is written", out.exists(), "no file"))
    if not out.exists():
        report(results)
        return 1

    text = out.read_text()
    try:
        summary = json.loads(text)
    except json.JSONDecodeError as exc:
        report(results + [("summary.json is valid JSON", False, str(exc))])
        return 1
    results.append(("summary.json is valid JSON", True, ""))

    days = summary.get("days", [])
    results.append(("one row per calendar day", len(days) == 4, f"got {len(days)}"))
    minutes = {row["date"]: row["minutes"] for row in days if isinstance(row.get("date"), str)}
    results.append(("the two sessions on 2026-11-06 are added up", minutes.get("2026-11-06") == 60, f"got {minutes.get('2026-11-06')}"))
    results.append(("every day carries its own total", minutes == EXPECTED, str(minutes)))
    results.append(("the rows come out in date order", [row["date"] for row in days] == sorted(EXPECTED), str([row["date"] for row in days])))

    stamp = summary.get("generated_at")
    iso = False
    if isinstance(stamp, str):
        try:
            iso = datetime.fromisoformat(stamp) is not None
        except ValueError:
            iso = False
    results.append(("generated_at is an ISO 8601 string", iso, repr(stamp)))
    results.append(("the script prints its own summary line", run.stdout.strip() == "4 days from 3 files", repr(run.stdout.strip())))

    return 0 if report(results) else 1


if __name__ == "__main__":
    sys.exit(main())

Ia menulis folder fixture yang tetap: tiga file, dengan dua sesi pada 2026-11-06 di salah satunya. Ia menjalankan daily.py pada folder itu, lalu memeriksa apa yang dicetak skrip dan JSON yang ditulisnya. Terhadap skrip awal, hasilnya merah:

TEXT
$ python3 check_daily.py
FAIL  the script runs without crashing  <- TypeError: Object of type datetime is not JSON serializable

0 of 1 checks passed

Exit code-nya 1, dan teks setelah panah adalah baris error yang diambil checker dari stderr. Ia sengaja berhenti di kegagalan pertama. Skrip yang crash tidak menulis summary.json, jadi pemeriksaan lain tidak punya apa-apa untuk dilihat.

Satu baris yang memperbaikinya

datetime.now() mengembalikan waktu lokal mesin tanpa zona waktu. Tambahkan .isoformat(timespec="seconds") dan kamu mendapat string, yang sudah bisa ditulis json. Hanya baris generated_at yang berubah.

PYTHON
# daily.py
import argparse
import json
from datetime import datetime
from pathlib import Path

parser = argparse.ArgumentParser(description="Minutes per day from a folder of session logs")
parser.add_argument("folder")
args = parser.parse_args()

total = {}
for path in sorted(Path(args.folder).glob("*.json")):
    if path.name == "summary.json":
        continue
    for session in json.loads(path.read_text()):
        total[session["date"]] = total.get(session["date"], 0) + session["minutes"]

report = {
    "generated_at": datetime.now().isoformat(timespec="seconds"),
    "days": [{"date": day, "minutes": minutes} for day, minutes in sorted(total.items())],
}

out = Path(args.folder) / "summary.json"
out.write_text(json.dumps(report, indent=2))
print(f"{len(report['days'])} days from {len(list(Path(args.folder).glob('*.json'))) - 1} files")

Run nyata pada folder berisi tiga file log mencetak persis 4 days from 3 files. Checker setuju, dengan exit code 0:

TEXT
$ python3 check_daily.py
PASS  the script runs without crashing
PASS  summary.json is written
PASS  summary.json is valid JSON
PASS  one row per calendar day
PASS  the two sessions on 2026-11-06 are added up
PASS  every day carries its own total
PASS  the rows come out in date order
PASS  generated_at is an ISO 8601 string
PASS  the script prints its own summary line

9 of 9 checks passed

summary.json dari run nyata itu diawali timestamp, lalu empat baris hari:

JSON
{
  "generated_at": "2026-11-08T00:01:43",
  "days": [
    {
      "date": "2026-11-02",
      "minutes": 15
    },
    {
      "date": "2026-11-06",
      "minutes": 60
    },
    {
      "date": "2026-11-07",
      "minutes": 40
    },
    {
      "date": "2026-11-08",
      "minutes": 50
    }
  ]
}

Angka 60 adalah hasil penjumlahan dari file yang berisi dua sesi, 25 dan 35 menit.

Yang tidak bisa dilihat checker

Ubah timespec="seconds" menjadi timespec="hours" lalu jalankan checker lagi. Ia tetap mencetak 9 of 9 checks passed dan keluar dengan kode 0. datetime.fromisoformat menerima string yang lebih pendek itu. Pemeriksaan tanggal membuktikan timestamp bisa di-parse, bukan bahwa timestamp itu punya presisi yang kamu inginkan.

Sebelum artikel berikutnya

Hapus sorted() di sekitar total.items() lalu jalankan checker sekali lagi. Kamu mendapat 8 of 9 checks passed dan exit code 1:

TEXT
$ python3 check_daily.py
PASS  the script runs without crashing
PASS  summary.json is written
PASS  summary.json is valid JSON
PASS  one row per calendar day
PASS  the two sessions on 2026-11-06 are added up
PASS  every day carries its own total
FAIL  the rows come out in date order  <- ['2026-11-06', '2026-11-07', '2026-11-02', '2026-11-08']
PASS  generated_at is an ISO 8601 string
PASS  the script prints its own summary line

8 of 9 checks passed

Dict menyimpan urutan penyisipan, jadi baris keluar dalam urutan file dibaca. Urutan baris yang tidak kamu sortir adalah urutan yang tidak kamu pilih.

Biar makin jago ngoding.

Yuk gabung bareng temen-temen developer lainnya buat dapet update teknologi, tips, dan tutorial santai tiap minggu.

Biar Nggak Kudet

Update teknologi, tips ngoding, dan insight santai langsung ke email kamu tiap minggu.

Santai, anti spam. Bisa berhenti langganan kapan aja.

Baca Selanjutnya

Lihat semua
Struktur DataTentang Struktur Data LinkedList

Linked list adalah rantai linear berisi node, dan setiap node menyimpan data plus pointer ke node berikutnya. Ini bentuk rantainya, ongkosnya dibanding array, dan implementasi paling sederhana dalam Java, Python, serta JavaScript.

Untuk developer yang belajar dari kesulitan

© 2026 Inva.dev.