Bab 14 — Format Data: CSV, JSON, dan Hati-hati dengan Pickle
Seri B3 — Program Kecil yang Berguna
Semua skrip bab ini ada di folder kode/bab14/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip). Berkas hasil ditulis ke subfolder _tmp/.
Di Bab 13, kita menyimpan data pohon dengan format buatan sendiri (A|Jati|32.5) dan sudah curiga formatnya rapuh. Bayangkan dua petugas yang saling mengirim catatan: bila masing-masing punya cara menulis sendiri, tidak ada yang bisa membaca catatan lainnya. Maka dunia sepakat pada beberapa format standar. Mengubah data di memori menjadi teks (atau byte) yang bisa disimpan disebut serialisasi; mengembalikannya disebut deserialisasi. Bab ini membahas tiga format: CSV, JSON, dan pickle.

CSV: tabel sebagai teks
CSV (comma-separated values) adalah tabel yang tiap barisnya satu catatan dan tiap kolomnya dipisah koma. Berkasnya bisa dibuka Excel. Kita menulis data kolam dari Bab 11:
01_csv_tulis.py
# Bab 14 - Menulis CSV: tabel sebagai teks berkolom koma
import csv
from pathlib import Path
from data_inti import ikan
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
berkas = tmp / "ikan.csv"
kolom = ["kolam", "jenis", "jumlah", "bobot_rata_g", "ph_air"]
with open(berkas, "w", newline="", encoding="utf-8") as f: # newline="" wajib untuk csv
penulis = csv.DictWriter(f, fieldnames=kolom)
penulis.writeheader() # baris judul kolom
penulis.writerows(ikan) # semua baris sekaligus
print(berkas.read_text(encoding="utf-8"))kolam,jenis,jumlah,bobot_rata_g,ph_air
A,lele,800,540.5,7.2
B,nila,1250,300.0,6.9
C,lele,600,480.0,7.0
DictWritermenulis daftar dict;fieldnamesmenentukan urutan kolom.writeheader()menulis baris judul,writerows()menulis semua baris.newline=""padaopen()wajib untuk modulcsv; dokumentasi Python menyebut berkas CSV harus dibuka dengannewline=''agar akhir baris ditangani dengan benar.
Data ini data tiruan (angka contoh).
Membaca CSV: semuanya teks
02_csv_baca.py
# Bab 14 - Membaca CSV: semua nilai datang sebagai TEKS
import csv
from pathlib import Path
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
berkas = tmp / "ikan_baca.csv"
berkas.write_text("kolam,jenis,jumlah,bobot_rata_g\nA,lele,800,540.5\nB,nila,1250,300.0\n",
encoding="utf-8")
with open(berkas, newline="", encoding="utf-8") as f:
baris = list(csv.DictReader(f)) # tiap baris menjadi dict berkunci judul kolom
print(baris[0])
print("Tipe jumlah:", type(baris[0]["jumlah"]).__name__)
print("Salah :", baris[0]["jumlah"] + baris[1]["jumlah"]) # teks digabung, bukan dijumlah
for b in baris: # ubah tipe sendiri
b["jumlah"] = int(b["jumlah"])
b["bobot_rata_g"] = float(b["bobot_rata_g"])
print("Benar :", baris[0]["jumlah"] + baris[1]["jumlah"]){'kolam': 'A', 'jenis': 'lele', 'jumlah': '800', 'bobot_rata_g': '540.5'}
Tipe jumlah: str
Salah : 8001250
Benar : 2050
DictReader membuat dict per baris dengan kunci dari baris judul. Perhatikan: 'jumlah': '800' diapit tanda kutip, artinya teks, bukan angka. Dokumentasi csv menegaskan tidak ada konversi tipe otomatis. Akibatnya "800" + "1250" menghasilkan 8001250 (teks digabung), bukan 2050. Anda sendiri yang harus mengubah tipenya dengan int() dan float().
Nilai True dan False di CSV juga teks. bool("False") menghasilkan True (karena teks tak kosong dianggap benar)! Cocokkan teksnya secara eksplisit, seperti pada proyek di bawah.
Mengapa bukan split(",")
03_csv_koma_dalam_isi.py
# Bab 14 - Koma di dalam isi: alasan memakai modul csv, bukan split(",")
import csv
import io
mentah = 'plot,catatan\nA,"dekat sungai, tanah gambut"\nB,"disebut ""hutan keramat"" oleh warga"\n'
print("Isi mentah:")
print(mentah)
print("Cara naif split(','):")
for baris in mentah.splitlines()[1:]:
print(" ", baris.split(","))
print("Memakai csv.reader:")
for baris in csv.reader(io.StringIO(mentah)):
print(" ", baris)Isi mentah:
plot,catatan
A,"dekat sungai, tanah gambut"
B,"disebut ""hutan keramat"" oleh warga"
Cara naif split(','):
['A', '"dekat sungai', ' tanah gambut"']
['B', '"disebut ""hutan keramat"" oleh warga"']
Memakai csv.reader:
['plot', 'catatan']
['A', 'dekat sungai, tanah gambut']
['B', 'disebut "hutan keramat" oleh warga']
Catatan “dekat sungai, tanah gambut” memuat koma. CSV membungkus isi seperti itu dengan tanda kutip, dan tanda kutip di dalam isi digandakan (""). split(",") buta terhadap aturan itu dan memotong catatan menjadi tiga potong yang salah; csv.reader menanganinya dengan benar. Inilah pelajaran dari Bab 13: format buatan sendiri akan menabrak kasus yang tidak Anda duga, sedangkan modul standar sudah memikirkannya.
JSON: data bersarang
CSV hanya cocok untuk tabel datar. Untuk data bersarang (misalnya dict berisi list), pakai JSON (JavaScript Object Notation), format teks yang dipakai hampir semua layanan internet.
04_json_dasar.py
# Bab 14 - JSON: data bersarang sebagai teks
import json
kolam = {"kolam": "A", "jenis": "lele", "jumlah": 800, "ph_air": 7.2,
"aktif": True, "catatan": None, "sampel_g": [520, 480, 560]}
teks = json.dumps(kolam) # dict -> teks JSON
print(teks)
print(json.dumps(kolam, indent=2, ensure_ascii=False)[:90], "...")
kembali = json.loads(teks) # teks JSON -> dict
print(kembali == kolam, type(kembali).__name__)
print(json.dumps({"suhu": "28 °C"})) # bawaan: huruf khusus di-escape
print(json.dumps({"suhu": "28 °C"}, ensure_ascii=False)) # tampil apa adanya
print(json.dumps((1, 2)), json.dumps({1: "a"})) # tuple jadi list, kunci angka jadi teks{"kolam": "A", "jenis": "lele", "jumlah": 800, "ph_air": 7.2, "aktif": true, "catatan": null, "sampel_g": [520, 480, 560]}
{
"kolam": "A",
"jenis": "lele",
"jumlah": 800,
"ph_air": 7.2,
"aktif": true,
...
True dict
{"suhu": "28 \u00b0C"}
{"suhu": "28 °C"}
[1, 2] {"1": "a"}
json.dumps()mengubah objek Python menjadi teks JSON (s= string);json.loads()kebalikannya.- Padanan tipe:
Truemenjaditrue,Nonemenjadinull, dict menjadi objek{...}, list menjadi larik[...]. Teks JSON memakai tanda kutip ganda. indent=2merapikan tampilan agar enak dibaca manusia.ensure_ascii=Falsemenampilkan°apa adanya; tanpa itu, JSON menulis°(tetap sah, hanya kurang enak dibaca).- Dua kejutan di baris terakhir: tuple
(1, 2)kembali sebagai list, dan kunci angka1menjadi teks"1". JSON tidak punya tuple, dan kuncinya selalu teks.
JSON ke berkas
05_json_berkas.py
# Bab 14 - JSON ke dan dari berkas, dengan data bersarang lima sektor
import json
from pathlib import Path
from data_inti import pohon, tanaman, ikan, ternak
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
berkas = tmp / "inventaris.json"
semua = {"pohon": pohon, "tanaman": tanaman, "ikan": ikan, "ternak": ternak}
with open(berkas, "w", encoding="utf-8") as f:
json.dump(semua, f, indent=2, ensure_ascii=False)
with open(berkas, encoding="utf-8") as f:
kembali = json.load(f)
print("Sama persis?", kembali == semua)
print("Bagian:", list(kembali))
print("Pohon pertama:", kembali["pohon"][0])
print("Tipe 'sehat':", type(kembali["pohon"][2]["sehat"]).__name__)
print("Ukuran berkas:", berkas.stat().st_size, "byte")Sama persis? True
Bagian: ['pohon', 'tanaman', 'ikan', 'ternak']
Pohon pertama: {'jenis': 'Jati', 'diameter_cm': 32.5, 'tinggi_m': 18.0, 'sehat': True, 'plot': 'A'}
Tipe 'sehat': bool
Ukuran berkas: 2200 byte
Berbeda dengan CSV, JSON mengembalikan tipe: sehat kembali sebagai bool, angka kembali sebagai int atau float, dan seluruh struktur (empat tabel dalam satu dict) kembali persis sama dengan aslinya. (Ukuran berkas dihitung pada Windows, dan bisa sedikit berbeda di komputer lain karena akhir baris.) Untuk menyimpan satu set data utuh, JSON biasanya lebih nyaman; untuk tabel besar yang akan dibuka di Excel, CSV lebih cocok.
Dua masalah JSON
06_json_masalah.py
# Bab 14 - Dua masalah JSON yang umum: teks rusak dan tipe yang tidak dikenal
import json
from datetime import date
rusak = '{"kolam": "A", "jumlah": 800,}' # koma berlebih di akhir: JSON tidak mengizinkan
try:
json.loads(rusak)
except json.JSONDecodeError as e:
print("JSON rusak:", type(e).__name__, "pada baris", e.lineno)
catatan = {"kolam": "A", "tanggal_tebar": date(2026, 3, 1)}
try:
json.dumps(catatan)
except TypeError as e:
print("Tidak bisa:", e)
print("Dengan default=str:", json.dumps(catatan, default=str))
print("Cara tetap: simpan tanggal sebagai teks ISO ->", date(2026, 3, 1).isoformat())JSON rusak: JSONDecodeError pada baris 1
Tidak bisa: Object of type date is not JSON serializable
Dengan default=str: {"kolam": "A", "tanggal_tebar": "2026-03-01"}
Cara tetap: simpan tanggal sebagai teks ISO -> 2026-03-01
- Teks rusak: koma berlebih di akhir membuat
JSONDecodeError. Ini sering terjadi bila JSON ditulis atau disunting tangan. Pesan lengkapnya menyebut baris dan kolom; kami hanya menampilkan nama error dan nomor barisnya karena kalimat pesan berbeda antarversi Python. - Tipe tak dikenal: JSON tidak mengenal tanggal, jadi
json.dumpsmelemparTypeError. Jalan keluar cepat:default=str. Kebiasaan yang lebih baik: simpan tanggal sebagai teks ISO (2026-03-01) dan ubah kembali dengandate.fromisoformat()(Bab 11).
Pickle: praktis, tetapi berbahaya
Pickle menyimpan hampir apa saja dari Python (objek buatan sendiri, fungsi, set) ke dalam byte. Praktis, tetapi dokumentasi Python menyatakan terang-terangan bahwa modul pickle tidak aman dan hanya boleh dipakai untuk data yang Anda percayai, karena data pickle yang dirancang jahat dapat menjalankan kode apa saja saat dibuka. Mari buktikan, dengan cara yang aman: kami membuat pickle yang hanya menjalankan print, tidak ada perintah yang berbahaya.
07_pickle_bahaya.py
# Bab 14 - pickle: praktis untuk data sendiri, BERBAHAYA untuk data orang lain
import pickle
import pickletools
class Pembawa:
"""Objek yang sengaja membuat pickle MENJALANKAN fungsi saat dibuka."""
def __reduce__(self):
# Di serangan nyata, ini bisa berupa perintah merusak. Di sini hanya print.
return (print, ("!!! kode ini berjalan hanya karena pickle.loads() dipanggil",))
aman = {"kolam": "A", "jumlah": 800}
paket_aman = pickle.dumps(aman)
print("Pickle data sendiri dibuka:", pickle.loads(paket_aman))
paket_berisiko = pickle.dumps(Pembawa(), protocol=4) # protokol dipatok agar keluaran sama di semua versi
print("Sebelum loads: belum ada apa-apa yang berjalan")
pickle.loads(paket_berisiko) # <- inilah bahayanya
print("Setelah loads: print di atas dijalankan oleh pickle, bukan oleh kita")
print("--- melihat isi pickle TANPA membukanya (pickletools) ---")
pickletools.dis(paket_berisiko)Pickle data sendiri dibuka: {'kolam': 'A', 'jumlah': 800}
Sebelum loads: belum ada apa-apa yang berjalan
!!! kode ini berjalan hanya karena pickle.loads() dipanggil
Setelah loads: print di atas dijalankan oleh pickle, bukan oleh kita
--- melihat isi pickle TANPA membukanya (pickletools) ---
0: \x80 PROTO 4
2: \x95 FRAME 88
11: \x8c SHORT_BINUNICODE 'builtins'
21: \x94 MEMOIZE (as 0)
22: \x8c SHORT_BINUNICODE 'print'
29: \x94 MEMOIZE (as 1)
30: \x93 STACK_GLOBAL
31: \x94 MEMOIZE (as 2)
32: \x8c SHORT_BINUNICODE '!!! kode ini berjalan hanya karena pickle.loads() dipanggil'
93: \x94 MEMOIZE (as 3)
94: \x85 TUPLE1
95: \x94 MEMOIZE (as 4)
96: R REDUCE
97: \x94 MEMOIZE (as 5)
98: . STOP
highest protocol among opcodes = 4
Telusuri hasilnya:
- Pickle data sendiri (dict kolam) terbuka biasa.
- Kita membuat pickle dari objek
Pembawayang__reduce__-nya berkata “tolong panggil fungsi ini saat pickle dibuka”. - Di antara dua
printkita, muncul baris!!! kode ini berjalan .... Kita tidak memanggil print itu;pickle.loads()yang memanggilnya. Bila isinya perintah penghapus berkas, hasilnya sama: berjalan tanpa izin. pickletools.dis()membaca pickle tanpa menjalankannya dan memperlihatkan dengan jelas bahwa pickle itu memanggilbuiltins.print(STACK_GLOBALlaluREDUCE). Dokumentasi Python juga menyarankanpython -m pickletoolssebagai cara yang lebih aman daripada membuka pickle untuk memeriksanya.
Jangan pernah membuka (pickle.load atau pickle.loads) berkas pickle yang berasal dari orang lain, dari internet, atau dari surel, dan jangan menerimanya dari pengguna aplikasi web. Untuk data dari luar, pakai JSON atau CSV. Pickle hanya untuk data yang Anda sendiri tulis dan simpan di tempat yang aman.
Proyek Bersambung B3: simpan dan muat lima sektor
Data lima sektor (hutan, kebun, pertanian, perikanan, peternakan; kebun dan pertanian berbagi tabel tanaman) kini disimpan ke CSV dan JSON. Perkakasnya ada di modul alat_data.py: simpan_csv, muat_csv (dengan skema yang menyebut pengubah tipe tiap kolom), simpan_json, dan muat_json.
08_proyek_simpan.py
# Bab 14 - Proyek B3: data kelima sektor disimpan ke CSV dan JSON, dibaca kembali, dibandingkan
from pathlib import Path
from alat_data import simpan_csv, muat_csv, simpan_json, muat_json
from data_inti import pohon, tanaman, ikan, ternak
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
tabel = {"pohon": pohon, "tanaman": tanaman, "ikan": ikan, "ternak": ternak}
bool_teks = lambda s: {"True": True, "False": False}[s] # KeyError bila bukan True/False
skema = {
"pohon": {"jenis": str, "diameter_cm": float, "tinggi_m": float, "sehat": bool_teks, "plot": str},
"tanaman": {"komoditas": str, "blok": str, "umur_hari": int, "hasil_kg": float},
"ikan": {"kolam": str, "jenis": str, "jumlah": int, "bobot_rata_g": float, "ph_air": float},
"ternak": {"kandang": str, "jenis": str, "umur_bulan": int, "bobot_kg": float},
}
for nama, baris in tabel.items():
simpan_csv(tmp / f"{nama}.csv", baris)
simpan_json(tmp / "inventaris.json", tabel)
for nama in tabel:
dibaca, ditolak = muat_csv(tmp / f"{nama}.csv", skema[nama])
sama = dibaca == tabel[nama]
print(f"{nama:8} CSV: {len(dibaca)} baris, ditolak {ditolak}, sama dengan asli: {sama}")
print("JSON utuh sama dengan asli:", muat_json(tmp / "inventaris.json") == tabel)
print("Isi pohon.csv (3 baris pertama):")
for baris in (tmp / "pohon.csv").read_text(encoding="utf-8").splitlines()[:3]:
print(baris)pohon CSV: 6 baris, ditolak 0, sama dengan asli: True
tanaman CSV: 4 baris, ditolak 0, sama dengan asli: True
ikan CSV: 3 baris, ditolak 0, sama dengan asli: True
ternak CSV: 4 baris, ditolak 0, sama dengan asli: True
JSON utuh sama dengan asli: True
Isi pohon.csv (3 baris pertama):
jenis,diameter_cm,tinggi_m,sehat,plot
Jati,32.5,18.0,True,A
Meranti,45.2,27.5,True,A
Hasil sama dengan asli: True untuk keempat tabel membuktikan bahwa data yang kembali dari CSV (setelah dikonversi sesuai skema) identik dengan yang disimpan, termasuk kolom sehat yang berupa bool. JSON juga identik. Sekarang uji ketahanannya:
09_proyek_tahan_salah.py
# Bab 14 - Proyek B3: memuat CSV yang sudah dikotori petugas, dan berkas yang hilang atau rusak
import logging
import sys
from pathlib import Path
from alat_data import simpan_csv, muat_csv, muat_json
from data_inti import tanaman
logging.basicConfig(stream=sys.stdout, level=logging.WARNING, format="%(levelname)s: %(message)s")
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
berkas = tmp / "tanaman_kotor.csv"
simpan_csv(berkas, tanaman)
with open(berkas, "a", newline="", encoding="utf-8") as f: # ulah petugas lapangan
f.write("padi,E,seratus,250.0\n") # umur bukan angka
f.write("jagung,F,95\n") # kolom kurang
skema = {"komoditas": str, "blok": str, "umur_hari": int, "hasil_kg": float}
baris, ditolak = muat_csv(berkas, skema)
print(f"Diterima {len(baris)} baris, ditolak {ditolak}")
print("Total panen sah:", sum(b["hasil_kg"] for b in baris), "kg")
print("Berkas hilang:", muat_csv(tmp / "tidak_ada.csv", skema))
(tmp / "rusak.json").write_text('{"pohon": [1, 2,', encoding="utf-8")
print("JSON rusak -> cadangan:", muat_json(tmp / "rusak.json", cadangan={}))WARNING: baris 6 di tanaman_kotor.csv ditolak (ValueError)
WARNING: baris 7 di tanaman_kotor.csv ditolak (TypeError)
Diterima 4 baris, ditolak 2
Total panen sah: 1030.0 kg
WARNING: berkas tidak_ada.csv belum ada
Berkas hilang: ([], 0)
WARNING: berkas rusak.json bukan JSON yang sah
JSON rusak -> cadangan: {}
Petugas menambahkan dua baris rusak: seratus tidak bisa menjadi int (ValueError), dan baris dengan kolom kurang membuat DictReader mengisi None, yang gagal diubah (TypeError). Keduanya ditolak dengan peringatan, empat baris sah tetap terbaca (total 1030,0 kg, cocok dengan Bab 11). Berkas hilang menghasilkan daftar kosong, dan JSON rusak menghasilkan nilai cadangan {}. Tidak ada satu pun yang meruntuhkan program: itulah arti “tahan terhadap input salah”.
Latihan
- Tulis data
ternakketernak.csv, baca kembali denganDictReader, dan cetak totalbobot_kg(jangan lupa mengubah tipenya). - Simpan dict
{"nama": "Kolam Ø1", "suhu": "28 °C"}ke JSON dengan dan tanpaensure_ascii=False. Bandingkan isi berkasnya. - Ubah
08_proyek_simpan.pyagartinggi_mpohon dibulatkan ke bilangan bulat saat dimuat (ubah skemanya, bukan datanya). - Tantangan: tulis fungsi
bandingkan_csv_json(nama)yang memuat tabelnamadari CSV dan dari JSON, lalu melaporkan apakah keduanya sama. Apa yang terjadi bila ada nilaiTrueyang disimpan lewat CSV tanpa pengubahbool_teks?
Ringkasan Bab
- Serialisasi = mengubah data di memori menjadi teks/byte untuk disimpan; deserialisasi = sebaliknya.
- CSV: tabel datar; buka dengan
newline=""; semua nilai datang sebagai teks, ubah tipenya sendiri. - Jangan memotong CSV dengan
split(","); pakai modulcsv. - JSON: data bersarang; mengembalikan
bool, angka,None, list, dict; tidak mengenal tuple dan tanggal. - Pickle tidak aman: membukanya bisa menjalankan kode. Jangan buka pickle dari sumber yang tidak Anda percayai.
- Proyek B3: lima sektor tersimpan di CSV/JSON dan dimuat kembali dengan skema, tahan baris rusak, berkas hilang, dan JSON rusak.
Selanjutnya, di Bab 15: otomasi dokumen kantor, mengubah data tadi menjadi laporan Excel, Word, dan PDF.