Fitur-fitur yang kita pelajari di section ini tersebar pada banyak versi: := hadir di 3.8, match di 3.10, tomllib dan TaskGroup di 3.11, statement type di 3.12. Masalah muncul begitu kode kita berjalan di lebih dari satu mesin — server produksi, laptop rekan, image Docker lama — yang versi Pythonya tidak seragam. Materi penomoran versi dan siklus dukungan telah dibahas pada Chapter Versi Python dan Kompatibilitas; Chapter ini fokus pada teknik praktis menjaga satu codebase tetap sah pada rentang versi yang kita pilih.

Peta Fitur dan Batas Bawah #

Langkah pertama strategi kompatibilitas adalah menetapkan batas bawah (minimum) versi secara sadar. Saat materi ini ditulis, rilis stabil terbaru adalah Python 3.15, sedangkan seluruh contoh pada course ini diuji pada Python 3.12 — dua versi di belakang terbaru, sesuai praktik umum menunggu ekosistem matang. Sebelum memakai fitur, cek versi pengenalannya:

FiturVersi perkenalan
Walrus operator :=, penanda /3.8
dict merge |, str.removeprefix()3.9
match/case, union X | Y3.10
tomllib, ExceptionGroup, except*, asyncio.TaskGroup3.11
Statement type, generic class Kotak[T], f-string PEP 7013.12
warnings.deprecated, mode ulang REPL3.13

Begitu batas bawah ditetapkan, seluruh fitur di atasnya bebas dipakai; fitur di bawahnya butuh salah satu teknik pada bagian berikut — atau keputusan menulis ulang dengan sintaks lama.

Feature Gating dengan sys.version_info #

Tuple bernama sys.version_info mendukung perbandingan langsung dengan tuple biasa, sehingga cabang kode bisa mengikuti versi interpreter yang sedang berjalan:

python
import sys

versi = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
print("Versi interpreter:", versi)
print("Minimal 3.11?", sys.version_info >= (3, 11))

if sys.version_info >= (3, 11):
    import tomllib
    print("memakai tomllib dari stdlib")
else:
    print("tomllib tidak tersedia; butuh fallback eksternal")

Output yang Dihasilkan:

text
Versi interpreter: 3.12.15
Minimal 3.11? True
memakai tomllib dari stdlib

Perhatikan dua detail pada contoh di atas. Pertama, pembanding (3, 11) sengaja tidak menyertakan angka micro — perbandingan (3, 12, 15) >= (3, 11) menangkap seluruh rilis 3.11.x ke atas tanpa peduli patch. Kedua, statement import tomllib diletakkan di dalam cabang, bukan di kepala file; pada interpreter 3.10, baris impor tersebut tidak pernah dieksekusi sehingga tidak memicu ModuleNotFoundError.

Import Fallback untuk Dependensi Opsional #

Teknik serumpun untuk module yang berpindah tempat antar versi: coba impor yang baru, tangkap ImportError, jatuh ke cadangan:

python
import json

try:
    import ujson
except ImportError:
    ujson = None


def serialize(data):
    if ujson is not None:
        return ujson.dumps(data)
    return json.dumps(data)


print(serialize({"nama": "Budi", "aktif": True}))

Output yang Dihasilkan:

text
{"nama": "Budi", "aktif": true}

Pola persis ini menjadi idiom resmi untuk tomllib pada library yang masih mendukung Python 3.10:

python
try:
    import tomllib
except ImportError:  # Python < 3.11
    import tomli as tomllib

Pada interpreter 3.11+, cabang try berhasil; pada 3.10, tomllib tidak ada sehingga tomli (paket eksternal dengan API identik) menggantikannya tanpa sisa cabang if di kode pemakai. Berbeda dengan gating versi, pola ini menguji ketersediaan module, bukan angka versi — pembedaan penting karena pengguna bisa saja memasang backport di versi baru.

TYPE_CHECKING: Impor Khusus Static Checker #

Anotasi tipe sering perlu mengimpor tipe yang tidak dibutuhkan saat runtime. Konstanta typing.TYPE_CHECKING bernilai True hanya di mata static type checker; interpreter Python selalu melihatnya False:

python
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # blok ini hanya dibaca static type checker, diabaikan interpreter
    from datetime import datetime


def format_tanggal(saat: "datetime") -> str:
    return saat.strftime("%d %B %Y")


print("TYPE_CHECKING bernilai:", TYPE_CHECKING)
print("datetime terimpor saat runtime:", "datetime" in globals())

Output yang Dihasilkan:

text
TYPE_CHECKING bernilai: False
datetime terimpor saat runtime: False

Kombinasi ini mengurangi biaya impor saat runtime sekaligus — import datetime dihilangkan sepenuhnya dari eksekusi. Perhatikan bahwa anotasi saat: "datetime" ditulis sebagai string agar interpreter tidak mencoba mengevaluasi nama yang tak terimpor.

future Annotations: Sintaks Baru pada Interpreter Lama #

Statement from __future__ import annotations menunda evaluasi seluruh anotasi: interpreter Python menyimpan anotasi sebagai string dan tidak mengevaluasinya saat def dieksekusi. Efeknya, anotasi boleh memakai sintaks yang lebih baru dari interpreter yang menjalankannya — termasuk X | None pada Python 3.8:

python
from __future__ import annotations


class Node:
    def __init__(self, nilai: int) -> None:
        self.nilai = nilai

    def berikutnya(self) -> Node | None:
        return None

    def tampilkan(self, node: Node | None = None) -> str:
        return f"nilai: {node.nilai if node else self.nilai}"


print(Node(7).tampilkan())
print(Node.__init__.__annotations__)

Output yang Dihasilkan:

text
nilai: 7
{'nilai': 'int', 'return': 'None'}

Baris def berikutnya(self) -> Node | None: merujuk class Node yang baru selesai didefinisikan setelah badan class selesai — tanpa __future__, evaluasi anotasi saat def berjalan memicu NameError: name 'Node' is not defined pada baris tersebut. Penundaan evaluasi juga terlihat pada baris terakhir: __annotations__ berisi string 'int', bukan objek int.

Penundaan ini tidak menurunkan kualitas pemeriksaan statis. Simpan cuplikan berikut sebagai cek.py dan jalankan python3 -m mypy cek.py:

python
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from decimal import Decimal


def hitung_total(harga: Decimal, jumlah: int) -> Decimal:
    return harga * jumlah

Output yang Dihasilkan:

text
Success: no issues found in 1 source file

Static type checker mypy tetap membaca anotasi string tersebut secara penuh — termasuk impor di balik TYPE_CHECKING — sehingga penundaan evaluasi runtime tidak mengorbankan pemeriksaan tipe.

Kebutuhan Library vs Aplikasi dan Strategi Jangka Panjang #

AspekAplikasi (internal, deploy sendiri)Library (dipakai pihak lain)
Batas bawah versiVersi image/server yang dikendalikanSerendah mungkin yang masih masuk akal
Feature gatingJarang perlu — naikkan versi targetRutin: fallback + sys.version_info
__future__ annotationsBebas dipakaiSalah satu alat utama
Kecepatan naik versiCepat, koordinasi internalKonservatif, tunggu adopsi pengguna

Untuk library, uji lintas versi jangan bergantung pada keberuntungan: jalankan test suite pada setiap versi yang didukung, baik lokal lewat tool seperti tox/nox maupun otomatis di CI (GitHub Actions, GitLab CI) dengan matrix versi — materi CI telah dibahas pada Chapter Continuous Integration.

Terakhir, kelola kepergian versi lama secara sadar. Cantumkan batas bawah di pyproject.toml lewat requires-python, tandai API lama dengan peringatan sebelum dihapus, dan tetapkan tanggal drop (misalnya mengikuti batas dukungan security suatu versi Python). Python 3.13 juga menambahkan decorator warnings.deprecated untuk menandai API usang secara resmi (PEP 702) — fitur tersebut belum tersedia pada interpreter 3.12 tempat course ini diuji; pemeriksaan cepat hasattr(warnings, "deprecated") pada 3.12 mengembalikan False.