Kamu punya satu fungsi format_rupiah yang sudah dipakai di tiga proyek berbeda. Setiap ada perbaikan, kamu salin file itu ke sana kemari, dan tanpa sadar versinya mulai beda-beda: satu proyek menghasilkan Rp 1.250.000, proyek lain masih Rp 1,250,000. Ini saat yang tepat untuk mengubah fungsi itu menjadi package Python yang bisa di-pip install, lengkap dengan versi yang jelas dan satu sumber kebenaran.

Kita akan membangun package bernama formatrupiah — alat kecil untuk memformat angka menjadi format Rupiah Indonesia — lalu mempublikasikannya ke PyPI. Di akhir tutorial, siapa pun bisa menginstallnya dengan pip install formatrupiah.

Struktur Proyek yang Benar

Buat folder dengan layout berikut. Kita pakai layout src/, bukan menaruh kode langsung di root. Layout ini mencegah Python mengimpor paket secara tidak sengaja dari direktori kerja saat test, jadi yang kamu uji selalu artefak yang sama dengan yang akan diinstall orang lain.

bash
formatrupiah/
├── src/
│   └── formatrupiah/
│       ├── __init__.py
│       └── rupiah.py
├── tests/
│   └── test_rupiah.py
├── README.md
├── LICENSE
└── pyproject.toml

File src/formatrupiah/rupiah.py berisi dua fungsi. format_rupiah mengubah angka jadi string Rupiah, dan parse_rupiah membacanya kembali jadi integer.

def format_rupiah(nomor, simbol=True):
    """Ubah angka menjadi string Rupiah, mis. 1250000 -> 'Rp 1.250.000'."""
    if not isinstance(nomor, (int, float)):
        raise TypeError("nomor harus int atau float")

    negatif = nomor < 0
    bulat = round(abs(nomor))

    # f-string memakai koma sebagai pemisah ribuan, kita ganti ke titik
    hasil = f"{bulat:,}".replace(",", ".")
    if negatif:
        hasil = f"-{hasil}"

    return f"Rp {hasil}" if simbol else hasil


def parse_rupiah(teks):
    """Ubah 'Rp 1.250.000' menjadi 1250000."""
    bersih = teks.replace("Rp", "").replace(" ", "").replace(".", "")
    return int(bersih)
python
from .rupiah import format_rupiah, parse_rupiah

__all__ = ["format_rupiah", "parse_rupiah"]
__version__ = "0.1.0"

Konfigurasi di pyproject.toml

Semua metadata package — nama, versi, deskripsi, dependencies — hidup di satu file: pyproject.toml. Di sini kita pakai Hatchling sebagai build backend karena ringan dan tidak butuh konfigurasi tambahan. Kalau kamu lebih nyaman dengan uv sebagai pengganti pip, file ini tetap sama persis; uv membaca pyproject.toml yang identik.

python
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "formatrupiah"
version = "0.1.0"
description = "Format angka ke format Rupiah Indonesia"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [{ name = "Nama Kamu", email = "kamu@example.com" }]
keywords = ["rupiah", "currency", "indonesia", "uang"]
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
]

[project.urls]
Homepage = "https://github.com/kamu/formatrupiah"

[tool.hatch.build.targets.wheel]
packages = ["src/formatrupiah"]

Install Editable dan Coba Langsung

Sebelum mempublikasikan apa pun, pastikan package berfungsi di lingkungan lokal. Install dalam mode editable supaya perubahan kode langsung terlihat tanpa install ulang.

bash
cd formatrupiah
python -m venv .venv
source .venv/bin/activate

pip install -e .
python -c "from formatrupiah import format_rupiah; print(format_rupiah(1250000))"
# Rp 1.250.000

Test dengan pytest

Dua fungsi di atas mudah sekali salah di kasus tepi — angka negatif, nol, teks tanpa simbol Rp. Kunci perilakunya dengan test sejak awal. Buat tests/test_rupiah.py lalu jalankan lewat pytest.

python
from formatrupiah import format_rupiah, parse_rupiah


def test_format_bulat():
    assert format_rupiah(1250000) == "Rp 1.250.000"


def test_format_tanpa_simbol():
    assert format_rupiah(999, simbol=False) == "999"


def test_format_negatif():
    assert format_rupiah(-500000) == "Rp -500.000"


def test_format_nol():
    assert format_rupiah(0) == "Rp 0"


def test_parse():
    assert parse_rupiah("Rp 1.250.000") == 1250000
bash
pip install pytest
pytest
# 5 passed in 0.03s

Jalankan linter dan formatter sebelum commit. Ruff memeriksa style sekaligus menghapus impor yang tidak terpakai dengan satu perintah: ruff check . --fix lalu ruff format .. Tambahkan [tool.ruff] ke pyproject.toml supaya aturannya ikut ter-versioning.

Build File Distribusi

File source saja belum cukup — PyPI butuh artefak distribusi. Dua format yang umum: sdist (arsip source) dan wheel (file siap install yang cepat). Bangun keduanya sekaligus.

bash
pip install build
python -m build
# dist/formatrupiah-0.1.0.tar.gz
# dist/formatrupiah-0.1.0-py3-none-any.whl

Uji Coba ke TestPyPI

Jangan langsung rilis ke PyPI. TestPyPI adalah salinan PyPI khusus untuk latihan — error di sana tidak merusak apa pun. Daftar akun di test.pypi.org, lalu upload dengan twine.

bash
pip install twine
twine upload --repository testpypi dist/*

Username saat upload bukan nama akun, tapi __token__. Password-nya adalah token API yang kamu buat di halaman account settings PyPI/TestPyPI. Jangan menulis token ke file yang ter-commit ke Git; simpan lewat variabel lingkungan atau biarkan twine menyimpannya dengan keyring saat ditanya.

Verifikasi hasilnya dengan menginstall dari TestPyPI di environment yang bersih.

bash
pip install --index-url https://test.pypi.org/simple/ formatrupiah

Rilis ke PyPI

Setelah semuanya berjalan di TestPyPI, rilis ke PyPI asli tinggal satu perintah. Pakai akun pypi.org dan token yang berbeda dari yang kamu buat tadi.

bash
twine upload dist/*

Merilis Versi Baru

Setiap perubahan yang kamu push ke PyPI wajib punya versi baru — PyPI menolak upload versi yang sama dua kali. Ikuti semantic versioning: patch (0.1.1) untuk perbaikan bug, minor (0.2.0) untuk fitur baru, major (1.0.0) untuk perubahan yang tidak kompatibel. Naikkan angka di pyproject.toml dan __init__.py, jalankan ulang test dan build, lalu upload lagi.

Mulailah dari fungsi yang paling sering kamu salin antar proyek. Ubah jadi package, publikasikan, dan lain kali tinggal pip install.