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:

python
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:

text
nama bersih: kopi-gayo
stok: 12
harga: 15000
---

total belanja : 150,000
per pelanggan : 50,000

Mari kita bedah tiga perubahan pada contoh di atas:

  1. Baris 4 (f"nama bersih: {nama_barang.replace(" ", "-")}"): ekspresi nama_barang.replace(" ", "-") memakai kutip ganda di dalam f-string berkutip ganda — pada Python 3.11 ke bawah, baris ini SyntaxError; sekarang tokenizer memahami batas kurung kurawal dengan benar.
  2. Baris 9 (f"{"\n".join(isi)}"): escape "\n" di dalam ekspresi akhirnya sah; dulu kita terpaksa menyiapkan pemisah = "\n" di variabel terpisah lalu {isi_join}.
  3. 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:

python
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:

text
nama='kopi gayo'
stok=12
harga=15750.50
stok * 3=36
harga * stok=189006.0
Format specContohHasil
(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:

python
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:

text
f-string   : 0.100 detik
.format()  : 0.154 detik
operator % : 0.125 detik
rasio .format/f-string : 1.5x
rasio %/f-string      : 1.2x

Alasannya 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:

python
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:

text
Hai Budi, pesanan INV-2025 sudah dikirim ke [email protected]
KeyError: 'id'
Hai Siti, pesanan $id sudah dikirim ke $email

Tiga 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.