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:
$ 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.dumpsmenulis sekumpulan tipe dasar yang tetap, dandatetimebukan salah satunya, jadi encoder melempar error alih-alih memilih format untukmu.datetime.now()mengembalikan waktu lokal mesin tanpa zona waktu, danisoformat(timespec="seconds")adalah cara resmi di dokumentasi untuk mengubahnya menjadi string yang bisa ditulis json.argparsemenjawab-htanpa diminta,pathlibmengubah folder menjadi path dan teks, danjsonmengubah teks menjadi list dan dict.- Checker yang hanya memastikan stamp bisa di-parse tidak bisa membedakan
timespec="hours"daritimespec="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.
# 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:
$ 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.
# 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:
$ 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.
# 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:
$ 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:
{
"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:
$ 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.