Module teruji dan test suite hijau — tetapi semua itu masih hidup di folder proyek kita. Langkah terakhir proyek ini adalah mengubah folder tersebut menjadi artefak standar yang bisa dipasang pip di komputer siapa pun, memverifikasinya seperti pengguna sungguhan, lalu menyiapkan langkah publikasi. Seluruh transkrip pada bagian ini — kecuali bagian unggah yang ditandai ilustratif — adalah hasil eksekusi nyata.

Melengkapi Metadata #

Versi pyproject.toml dari Bagian 1 cukup untuk testing, tetapi terlalu miskin untuk dipublikasikan: halaman PyPI nanti menampilkan README, pengguna perlu tahu lisensinya, dan kontributor perlu tahu di mana repository-nya. Perbarui pyproject.toml menjadi versi final:

toml
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "example-package"
version = "0.1.0"
description = "Utilitas teks sederhana: slug URL, penghitung kata, dan peringkas teks"
readme = "README.md"
license = "MIT"
authors = [
    { name = "Budi Santoso", email = "[email protected]" },
]
requires-python = ">=3.10"
dependencies = []
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Topic :: Text Processing",
]

[project.urls]
Homepage = "https://github.com/budi/example-package"
Issues = "https://github.com/budi/example-package/issues"
Changelog = "https://github.com/budi/example-package/blob/main/CHANGELOG.md"

[project.optional-dependencies]
dev = ["pytest>=8.0"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]

Kunci-kunci baru yang ditambahkan:

  1. readme = "README.md": menautkan berkas deskripsi; isinya di-render PyPI sebagai halaman depan package. Karena kunci ini merujuk sebuah berkas, README-nya harus benar-benar ada — buat README.md di akar proyek:
markdown
# example-package

Utilitas teks sederhana untuk mengolah string sehari-hari: slug URL,
penghitung kata, dan peringkas teks.

## Instalasi

pip install example-package

## Penggunaan

    from example_package import hitung_kata, ringkas, slugify

    slugify("Belajar Python Dasar!")            # belajar-python-dasar
    hitung_kata("Belajar Python setiap hari")   # 4
    ringkas("Teks panjang sekali di sini", 20)
  1. license = "MIT": identifier SPDX — format metadata modern yang menggantikan cara lama berbasis classifier lisensi.
  2. authors: nama dan email penulis; gunakan data yang sanggup dihubungi pengguna, bukan data palsu, saat mempublikasikan package sungguhan.
  3. classifiers: kategori taxonomi PyPI yang membantu pencarian — status kematangan, versi Python yang didukung, dan topik. Perhatikan bahwa classifier lisensi tidak diperlukan lagi karena kunci license sudah memakai SPDX.
  4. [project.urls]: tautan pendukung yang tampil di bilah samping halaman PyPI.
  5. [project.optional-dependencies]: extra bernama dev — pengguna yang ingin ikut mengembangkan package memasangnya dengan pip install example-package[dev], sedangkan pengguna biasa tidak perlu men-download pytest.

Membangun wheel dan sdist #

Alat build tidak kita pasang di lingkungan utama. Mengikuti Chapter Menggunakan pip, siapkan venv khusus di dalam proyek lalu pasang build di sana:

bash
python3 -m venv .venv-build
source .venv-build/bin/activate
pip install build

Jalankan dulu test suite untuk memastikan versi yang dikemas sehat, lalu bangun artefaknya:

bash
python3 -m pytest
python -m build

Output yang Dihasilkan (log internal setuptools — penyalinan berkas dan penulisan manifest — diringkas):

text
============================== 14 passed in 0.02s ==============================

* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - setuptools>=61
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - setuptools>=61
* Getting build dependencies for wheel...
* Building wheel...
Successfully built example_package-0.1.0.tar.gz and example_package-0.1.0-py3-none-any.whl

Baris pertama adalah ringkasan test suite dari Bagian 3 — versi yang dikemas terbukti sehat. Baris-baris berikutnya adalah keluaran python -m build, dan di balik layar front-end build tidak pernah menyentuh lingkungan kita: python -m build membuat venv terisolasi, memasang setuptools di sana, lalu meminta backend menyalin src/example_package/ beserta metadata menjadi dua artefak di folder dist/. Urutannya menarik untuk dicatat: Building wheel from sdist berarti wheel dibangun dari dalam sdist yang baru saja dibuat — dengan begitu kedua artefak dijamin berasal dari sumber yang persis sama.

Hasil akhirnya, dua berkas di folder dist/:

text
dist/
├── example_package-0.1.0-py3-none-any.whl
└── example_package-0.1.0.tar.gz

Perhatikan nama berkasnya: example_package dengan garis bawah — persis normalisasi nama yang dibahas pada Bagian 1, diterapkan otomatis pada nama artefak.

Memeriksa Isi Wheel #

Wheel hanyalah arsip ZIP dengan konvensi khusus. Karena wheel berisi kode Python murni, nama berkasnya membawa tag py3-none-any — berlaku untuk semua Python 3 di semua platform. Isinya bisa diperiksa langsung:

bash
unzip -l dist/example_package-0.1.0-py3-none-any.whl

Output yang Dihasilkan (tanggal dan waktu pada kolom Date mengikuti waktu build di masing-masing mesin):

text
Archive:  dist/example_package-0.1.0-py3-none-any.whl
  Length      Date    Time    Name
---------  ---------- -----   ----
     2760  2026-10-12 23:44   example_package/__init__.py
     1297  2026-10-12 23:45   example_package-0.1.0.dist-info/METADATA
       91  2026-10-12 23:45   example_package-0.1.0.dist-info/WHEEL
       16  2026-10-12 23:45   example_package-0.1.0.dist-info/top_level.txt
      414  2026-10-12 23:45   example_package-0.1.0.dist-info/RECORD
---------                     -------
     4578                     5 files

Dua fakta penting tampak pada daftar berkas itu. Pertama, module example_package/__init__.py utuh di dalamnya — folder tests/ dan pyproject.toml sengaja tidak ikut karena hanya diperlukan saat pengembangan, bukan saat dipakai. Kedua, seluruh metadata hasil render tabel [project] — nama, versi, README, sampai classifiers — tersimpan di example_package-0.1.0.dist-info/METADATA, itulah berkas yang dibaca pip saat instalasi dan PyPI saat menampilkan halaman package.

Verifikasi Instalasi di Venv Bersih #

Langkah paling jujur untuk menguji wheel adalah berpura-pura menjadi pengguna: venv baru tanpa apa pun, lalu pasang wheel-nya. Buat venv uji, pasang wheel langsung dari berkasnya, lalu import dan jalankan:

bash
python3 -m venv .venv-uji
source .venv-uji/bin/activate
pip install dist/example_package-0.1.0-py3-none-any.whl
python -c "
import example_package
from example_package import slugify

print(example_package.__version__)
print(slugify('Belajar Python Dasar!'))
"

Output yang Dihasilkan:

text
Processing ./dist/example_package-0.1.0-py3-none-any.whl
Installing collected packages: example-package
Successfully installed example-package-0.1.0
0.1.0
belajar-python-dasar

Tiga baris pertama adalah keluaran pip, dua baris terakhir keluaran Python: versi 0.1.0 terbaca dari __version__, dan slugify() bekerja dari dalam venv yang sama sekali tidak tahu keberadaan folder proyek kita. Wheel terbukti bisa dipasang dan berfungsi.

Unggah ke TestPyPI #

Langkah berikut bersifat ilustratif — transkripnya bukan hasil eksekusi nyata karena unggahan membutuhkan akun milik pembaca sendiri. Alurnya mengikuti Chapter Mendistribusikan Python Package:

  1. Buat akun di test.pypi.org — instance PyPI khusus latihan dengan basis data terpisah.
  2. Buat API token di halaman pengaturan akun dengan scope upload. Token hanya ditampilkan sekali; contoh di sini memakai placeholder YOUR_API_TOKEN.
  3. Periksa kemasan dengan twine check dist/* — status PASSED berarti metadata valid dan README akan ter-render.
  4. Unggah kedua artefak:
bash
twine upload --repository testpypi dist/*

Output yang Dihasilkan (ilustratif):

text
Uploading example_package-0.1.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━ 2.9/2.9 kB • 00:00:01 • 6.1 MB/s
Uploading example_package-0.1.0.tar.gz
100% ━━━━━━━━━━━━━━━━━━ 3.3/3.3 kB • 00:00:01 • 5.8 MB/s

View at:
https://test.pypi.org/project/example-package/0.1.0/

Sebagai pengguna sungguhan, uji hasil unggahan dengan pip install --index-url https://test.pypi.org/simple/ example-package. Bila semua beres, alur yang sama diulang ke PyPI produksi: buat akun pypi.org, aktifkan two-factor authentication, buat token, lalu twine upload dist/* tanpa flag --repository.

Strategi Naik Versi #

PyPI tidak pernah menimpa versi yang sudah ada: mengunggah 0.1.0 untuk kedua kali ditolak, apa pun isinya. Karena itu setiap rilis baru wajib menaikkan version di pyproject.toml mengikuti semantic versioning dari Chapter Dependency Pinning:

PerubahanVersi baruContoh
Perbaikan bug tanpa mengubah APInaikkan PATCH0.1.0 → 0.1.1
Function baru yang tidak merusak kode lamanaikkan MINOR0.1.0 → 0.2.0
Perubahan yang merusak kode penggunanaikkan MAJOR0.1.0 → 1.0.0

Setelah menaikkan versi, siklusnya selalu sama: jalankan test, bangun ulang dengan python -m build, uji instalasi di venv bersih, lalu unggah.

Proyek ini menutup perjalanan packaging kita pada Course Python: struktur, implementasi, testing, build, sampai distribusi — satu siklus hidup package yang utuh dan bisa diulang untuk library sungguhan berikutnya. Berkas pyproject.toml final tersedia pada bagian Berkas Sumber di bawah halaman ini.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

pyproject.toml

Unduh berkas
toml
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "example-package"
version = "0.1.0"
description = "Utilitas teks sederhana: slug URL, penghitung kata, dan peringkas teks"
readme = "README.md"
license = "MIT"
authors = [
    { name = "Budi Santoso", email = "[email protected]" },
]
requires-python = ">=3.10"
dependencies = []
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Topic :: Text Processing",
]

[project.urls]
Homepage = "https://github.com/budi/example-package"
Issues = "https://github.com/budi/example-package/issues"
Changelog = "https://github.com/budi/example-package/blob/main/CHANGELOG.md"

[project.optional-dependencies]
dev = ["pytest>=8.0"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]