f-string adalah fitur formatting paling populer di Python modern, dan materi dasarnya — interpolasi, format spec, hingga trik {x=} — telah dibahas pada Chapter f-strings. Namun di balik kesederhanaannya, f-string lama menyimpan batas aneh: ekspresi di dalamnya tidak boleh memuat kutip yang sama dengan kutip pembungkus, tidak boleh memuat backslash, dan tidak bisa dipecah beberapa baris. Python 3.12 (PEP 701) membongkar batas-batas itu. Chapter ini membedah apa yang berubah di dalam mesin f-string, mengukur performanya secara nyata, dan menunjukkan kapan f-string justru bukan alat yang tepat.
Di Balik Layar: f-string Adalah Kode Terkompilasi #
Ketika interpreter Python mengurai f"total: {total}", ekspresi {total} bukan teks yang dicari-lalu-diganti pada runtime. Sebelum PEP 701, parser mengubah f-string menjadi ekspresi gabungan FORMAT_VALUE — operasi level bytecode yang menempelkan hasil format(total) ke potongan literal. Efeknya: f-string dieksekusi dalam satu operasi C dan menjadi cara formatting tercepat di Python. Batas historisnya pun berasal dari tahap ini: karena isi {...} diurai dengan aturan khusus, kutip sama dan backslash tidak diizinkan.
Python 3.12 membongkar parser khusus tersebut: f-string kini ditangani tokenizer utama seperti ekspresi Python biasa. Konsekuensinya, hampir semua aturan lama gugur:
nama_barang = "kopi gayo"
# Python 3.12 (PEP 701): kutip ganda di dalam ekspresi f-string berkutip ganda
pesan = f"nama bersih: {nama_barang.replace(" ", "-")}"
print(pesan)
# Python 3.12: backslash kini boleh di dalam ekspresi f-string
isi = ["stok: 12", "harga: 15000"]
ringkasan = f"{"\n".join(isi)}"
print(ringkasan)
print("---")
# f-string multiline
total = 150000
laporan = f"""
total belanja : {total:,}
per pelanggan : {total // 3:,}
"""
print(laporan)Output yang Dihasilkan:
nama bersih: kopi-gayo
stok: 12
harga: 15000
---
total belanja : 150,000
per pelanggan : 50,000Mari kita bedah tiga perubahan pada contoh di atas:
- Baris 4 (
f"nama bersih: {nama_barang.replace(" ", "-")}"): ekspresinama_barang.replace(" ", "-")memakai kutip ganda di dalam f-string berkutip ganda — pada Python 3.11 ke bawah, baris iniSyntaxError; sekarang tokenizer memahami batas kurung kurawal dengan benar. - Baris 9 (
f"{"\n".join(isi)}"): escape"\n"di dalam ekspresi akhirnya sah; dulu kita terpaksa menyiapkanpemisah = "\n"di variabel terpisah lalu{isi_join}. - Baris 15 (
laporan = f"""..."""): f-string tiga-kutip sudah lama ada, tetapi kombinasi dengan dua fitur di atas membuat laporan multiline kini bisa memuat ekspresi kompleks tanpa trik.
Debugging {x=} dan Format Spec #
Perubahan kecil PEP 701 lain: banyak ekspresi yang dulu ditolak kini sah, termasuk komentar di dalam ekspresi dan penggunaan {x=} berdampingan dengan format spec. Rekap pola debugging dan format spec yang paling sering terpakai:
nama = "kopi gayo"
stok = 12
harga = 15750.5
print(f"{nama=}")
print(f"{stok=}")
print(f"{harga=:.2f}")
print(f"{stok * 3=:,}")
print(f"{harga * stok=:.1f}")Output yang Dihasilkan:
nama='kopi gayo'
stok=12
harga=15750.50
stok * 3=36
harga * stok=189006.0| Format spec | Contoh | Hasil |
|---|---|---|
(tanpa spec, {x=}) | {nama=} | nama='kopi gayo' — nilai di-repr utuh |
.2f | {harga=:.2f} | harga=15750.50 — dua digit desimal |
, | {stok * 3=:,} | stok * 3=36 — pemisah ribuan |
>8 | {stok:>8} | rata kanan lebar 8 |
0>3 | {stok:0>3} | isi nol di depan, misal 012 |
Performa: f-string vs .format() vs operator % #
Angka berikut diukur nyata dengan module timeit pada Python 3.12, satu juta iterasi per teknik. Jalankan ulang di mesin Anda dan nilainya akan berbeda — yang stabil adalah urutan relatifnya:
import timeit
nilai = 42
t_fstring = timeit.timeit("f'nilai: {nilai}'", globals={"nilai": nilai}, number=1_000_000)
t_format = timeit.timeit("'nilai: {}'.format(nilai)", globals={"nilai": nilai}, number=1_000_000)
t_persen = timeit.timeit("'nilai: %s' % nilai", globals={"nilai": nilai}, number=1_000_000)
print(f"f-string : {t_fstring:.3f} detik")
print(f".format() : {t_format:.3f} detik")
print(f"operator % : {t_persen:.3f} detik")
print(f"rasio .format/f-string : {t_format / t_fstring:.1f}x")
print(f"rasio %/f-string : {t_persen / t_fstring:.1f}x")Output yang Dihasilkan:
f-string : 0.100 detik
.format() : 0.154 detik
operator % : 0.125 detik
rasio .format/f-string : 1.5x
rasio %/f-string : 1.2xAlasannya ada di mekanisme tiap teknik: str.format() harus mencari dan mengurai {} pada runtime lalu memanggil method format(), sedangkan f-string dan operator % dikompilasi menjadi operasi bytecode langsung. Untuk kode panas (loop jutaan iterasi), selisih 20-50% ini nyata terasa.
Saat Template Datang dari Data: Jangan Pakai f-string #
Ada satu batas yang tidak berubah dan tidak akan berubah: f-string menuntut template tertulis di dalam kode dan diketahui saat penulisan. Bila template datang dari luar — file konfigurasi, database, input pengguna — pilihan yang aman adalah str.format() dengan kunci bernama, atau string.Template yang lebih ketat:
from string import Template
data = {"nama": "Budi", "id": "INV-2025", "email": "[email protected]"}
# template diketahui saat runtime (misal dari file konfigurasi)
template = "Hai {nama}, pesanan {id} sudah dikirim ke {email}"
print(template.format(**data))
# kunci hilang membuat str.format gagal
try:
print(template.format(nama="Budi"))
except KeyError as e:
print("KeyError:", e)
# safe_substitute membiarkan kunci yang hilang apa adanya
t = Template("Hai $nama, pesanan $id sudah dikirim ke $email")
print(t.safe_substitute(nama="Siti"))Output yang Dihasilkan:
Hai Budi, pesanan INV-2025 sudah dikirim ke [email protected]
KeyError: 'id'
Hai Siti, pesanan $id sudah dikirim ke $emailTiga perilaku berbeda terlihat pada contoh: str.format(**data) mengisi semua kunci yang tersedia; kunci yang hilang memicu KeyError yang jelas; dan safe_substitute() dari class Template membiarkan placeholder yang tidak terisi tetap tertulis, bukan gagal.