Begitu library Anda dipakai orang lain — rekan tim, proyek lain, atau publik — setiap detail yang Anda ubah berpotensi memutus kode mereka. Function yang dipindah nama, exception baru yang terbang, atau nilai kembali yang berubah bentuk semuanya adalah breaking change: pembaruan yang memaksa pengguna menulis ulang kode mereka. Chapter ini membahas cara bergerak maju tanpa meninggalkan pengguna lama: mengenali apa yang terhitung patah, memberi tahu lewat proses deprecasi, dan menjaga kontrak lewat semver serta changelog.

Apa yang Terhitung Breaking Change #

Tidak semua perubahan setara. Empat jenis perubahan berikut memutus pemanggil lama:

Jenis perubahanContohDampak pada pemanggil
Signaturetotal(harga, jumlah) menjadi total(items)TypeError saat memanggil cara lama
Nilai kembalifloat menjadi (float, str)unpacking dan perhitungan salah
Exceptionkini melempar KeyError untuk input kosongblok except lama tak menangkap
Perilakupembulatan naik-turun berubahhasil diam-diam berbeda — bug paling licik

Perhatikan baris terakhir: perubahan perilaku tanpa crash justru paling berbahaya karena tidak ada traceback yang mengarah ke penyebabnya.

Semver sebagai Janji Publik #

Skema semantic versioning — dibahas pada Chapter Dependency Pinning dari sisi konsumen — di sini dipandang dari sisi penulis library:

text
2.4.1
│ │ └── PATCH: perbaikan bug, perilaku tidak berubah
│ └──── MINOR: fitur baru, semua kode pengguna lama tetap jalan
└────── MAJOR: boleh ada breaking change

Janjinya tegas: selama angka MAJOR tidak bergerak, pemanggil boleh menaikkan versi tanpa membaca apa pun. Konsekuensnya, setiap breaking change menunggu jendela MAJOR berikutnya — dan di antara dua jendela itu, kita butuh teknik jembatan.

Proses Deprecasi Bertahap #

Menghapus function lama secara mendadak itu kasar. Jalur hormatnya bertahap: fungsi baru diperkenalkan, fungsi lama diberi peringatan, peringatan itu didokumentasikan dengan tenggat, barulah dihapus pada MAJOR berikutnya. File toko_lama.py:

python
"""Module dengan API lama yang sedang dideprecasi."""

import warnings


def hitung_total(harga, jumlah):
    """Hitung total belanja (versi lama)."""
    warnings.warn(
        "hitung_total sudah usang; gunakan hitung_tagihan",
        DeprecationWarning,
        stacklevel=2,
    )
    return harga * jumlah


def hitung_tagihan(harga, jumlah):
    """Hitung total belanja (versi baru)."""
    return harga * jumlah

File pemanggil pemanggil.py:

python
from toko_lama import hitung_total

print(hitung_total(15000, 3))

Hasil eksekusi python3 pemanggil.py:

text
45000

Kode lama tetap jalan — tetapi diam-diam sudah bersuara. DeprecationWarning secara bawaan disembunyikan Python; jalankan dengan filter agar terlihat:

bash
python3 -W default pemanggil.py

Output yang Dihasilkan:

text
45000
text
pemanggil.py:3: DeprecationWarning: hitung_total sudah usang; gunakan hitung_tagihan
  print(hitung_total(15000, 3))

Python menampilkan path lengkap file sebelum nama file; dipangkas di sini. Argumen stacklevel=2 yang penting: peringatan menunjuk baris pemanggil (pemanggil.py:3), bukan baris definisi — pengguna perlu tahu kode mana milik mereka yang harus berubah. Untuk CI, -W error::DeprecationWarning mengubah peringatan menjadi error sehingga penggunaan API usang gagal build sejak hari pertama.

Teknik Menjaga Kompatibilitas #

Selama belum tiba waktunya MAJOR, beberapa teknik menjaga agar API tetap ramah:

TeknikSituasinyaBentuknya
Parameter defaultmenambah argumen barudef bukti(nominal, metode="tunai")
Nama baru + aliasmengganti nama functionhitung_tagihan = hitung_total lalu deprecate yang lama
Konstanta penampungmengubah struktursimbol lama tetap hidup sebagai property yang mendelegasikan
TYPE_CHECKINGmengetik tanpa kopling runtimeimport anotasi hanya untuk mypy

Contoh teknik pertama — pemanggil lama dan baru hidup berdampingan:

python
def bukti_transaksi(nominal, metode="tunai", mata_uang="IDR"):
    """Cetak bukti transaksi.

    Versi lama hanya punya `nominal`; versi baru menambahkan
    `metode` dan `mata_uang` dengan nilai default.
    """
    print(f"Rp{nominal:,.0f} via {metode} ({mata_uang})")


bukti_transaksi(150_000)
bukti_transaksi(200_000, metode="qris")

Output yang Dihasilkan:

text
Rp150,000 via tunai (IDR)
Rp200,000 via qris (IDR)

Perhatikan kejujurannya: nilai default "tunai" dipilih agar identik dengan perilaku lama — parameter baru yang mengubah hasil bawaan tetap terhitung breaking.

Kontrak API: __all__ dan Garis Bawah #

Kontrak kompatibilitas hanya berlaku pada API yang Anda umumkan. Dua penanda membuat batas itu eksplisit. Nama diawali garis bawah (_bulatkan_rupiah) adalah wilayah internal: boleh berubah kapan pun. Daftar __all__ pada __init__.py menetapkan yang dianggap publik:

python
"""API publik toko: kontrak untuk pengguna package ini."""

from toko_paket.diskon import BATAS_DISKON, hitung_diskon, _bulatkan_rupiah

__all__ = ["BATAS_DISKON", "hitung_diskon"]

Pemanggil yang menulis from toko_paket import * hanya menerima dua nama itu:

python
from toko_paket import *

print([n for n in dir() if not n.startswith("_")])

Output yang Dihasilkan:

text
['BATAS_DISKON', 'hitung_diskon']

_bulatkan_rupiah bisa dihapus atau dirombak esok hari tanpa rasa bersalah — garis bawah sudah menandakannya bukan janji.

Changelog: Keep a Changelog #

Kontrak versi tidak berguna bila tidak terbaca. Format Keep a Changelog memberi struktur baku pada file CHANGELOG.md:

markdown
# Changelog

## [1.2.0] - 2025-06-15
### Added
- Fungsi `hitung_tagihan` sebagai pengganti `hitung_total`.

### Deprecated
- `hitung_total` kini mengeluarkan `DeprecationWarning` dan akan
  dihapus pada versi 2.0.0.

## [1.1.0] - 2025-04-02
### Added
- Fungsi `bukti_transaksi` dengan parameter opsional `metode`.

Empat kategori tetapnya: Added, Changed, Deprecated, Removed (plus Fixed untuk perbaikan). Tanggal menggunakan format ISO; entri Deprecated selalu menyebut versi penghapusan.

Kapan Boleh Melanggar #

Menjaga kompatibilitas pun harga. Setiap jembatan alias dan parameter default menambah permukaan yang harus dites; lama-lama API lama menghalangi desain yang benar. Momen yang tepat untuk melanggar adalah rilis MAJOR — dengan syarat alasannya kuat: API yang salah desainnya akan terus menghasilkan bug, atau pemeliharaan jembatannya lebih mahal daripada biaya migrasi pengguna. Saat waktunya tiba, pastikan rilis itu disertai panduan migrasi yang menunjukkan pasangan “cara lama → cara baru” untuk setiap perubahan. Pengguna bisa menerima kerja migrasi; yang tidak bisa diterima adalah kerja migrasi tanpa peta.