Free-Threaded Python Resmi Didukung: Arti Python 3.14t untuk Django pada 2026

Panduan praktis menguji Python 3.14t pada backend Django dan GraphQL, dari audit dependensi sampai worker CPU-bound.

Free-Threaded Python Resmi Didukung: Arti Python 3.14t untuk Django pada 2026

Python 3.14 punya varian yang dapat menjalankan beberapa thread Python secara paralel tanpa Global Interpreter Lock, atau GIL. Varian ini biasanya ditulis sebagai Python 3.14t. Huruf t menandakan build free-threaded.

Ini bukan pengganti otomatis untuk Python biasa. Build dengan GIL masih menjadi default, sedangkan Python 3.14t harus dipilih secara sadar dan diuji pada workload aplikasi sendiri.

Anggap ini audit migrasi, bukan sekadar mengganti interpreter.

Problem dan konteks

Apa yang berubah di Python 3.14?

Free-threading diperkenalkan secara eksperimental di Python 3.13. Melalui PEP 779, dukungannya resmi di Python 3.14, meskipun tahap berikutnya, yaitu menjadikan GIL nonaktif secara default, belum diputuskan.

Build free-threaded menonaktifkan GIL. Python tetap menyediakan cara untuk mengaktifkannya kembali ketika diperlukan:

PYTHON_GIL=1 python
python -X gil=1

Ada kondisi lain yang mudah terlewat. Ketika aplikasi mengimpor ekstensi C yang belum kompatibel dengan free-threading, interpreter dapat mengaktifkan kembali GIL dan menampilkan peringatan. Karena itu, label 3.14t saja belum membuktikan bahwa proses berjalan tanpa GIL.

Free-threaded bukan berarti semua endpoint lebih cepat

Klasifikasikan pekerjaan sebelum menambah thread:

Workload Contoh Langkah awal
CPU-bound dan thread-friendly preprocessing teks, batching embedding, pemrosesan PDF, pemrosesan gambar, ETL Uji dengan thread pool
I/O-bound query database, request API, membaca object storage Optimalkan query, batching, atau async
Campuran mengambil dokumen lalu memprosesnya lokal Pisahkan fase I/O dan CPU

Pada resolver GraphQL, query yang mengambil data dari beberapa service biasanya didominasi waktu tunggu. Sebaliknya, field yang memproses banyak dokumen atau menghitung hasil secara lokal dapat menjadi kandidat pengujian.

Ukur biaya dan kompatibilitas

Fase II menetapkan batas regresi pyperformance (CPython’s own benchmark suite) paling banyak 15 persen dan overhead memori paling banyak 20 persen. Brief ini juga mencatat overhead single-thread sekitar 5 sampai 10 persen. Angka tersebut adalah konteks proyek Python, bukan janji untuk aplikasi Django.

Hasil aplikasi dipengaruhi oleh jumlah pekerjaan CPU-bound, library C, pola akses database, ukuran object Python, jumlah worker, bentuk query GraphQL, cache, dan shared state. Python murni biasanya tidak membutuhkan perubahan khusus, tetapi asumsi lama tentang state bersama tetap harus diaudit.

Cache global, lazy initialization, penghitung statistik — semua state bersama yang bisa ditulis banyak thread butuh sinkronisasi eksplisit.

Django 6.1 dan free-threading

Django 6.1 mendukung Python 3.12 sampai 3.14 menurut brief ini. Itu tidak sama dengan pernyataan bahwa seluruh perilaku free-threaded Python sudah didukung resmi oleh Django.

Django tetap mempertahankan model satu koneksi per thread. Worker yang menjalankan kode Django perlu mengelola koneksi dengan benar. Memanggil close_old_connections() pada setiap unit pekerjaan adalah langkah yang sesuai untuk pekerjaan batch, dengan penyesuaian berdasarkan cara aplikasi mengambil data.

Solution walkthrough

1. Deteksi build dan status GIL

Mulai dengan diagnostik kecil. Py_GIL_DISABLED menunjukkan bahwa interpreter dibangun untuk free-threading. sys._is_gil_enabled() menunjukkan status GIL saat runtime.

# diagnostics.py
from __future__ import annotations

import sys
import sysconfig


def is_free_threaded_build() -> bool:
    return sysconfig.get_config_var("Py_GIL_DISABLED") == 1


def is_gil_enabled() -> bool:
    gil_status = getattr(sys, "_is_gil_enabled", None)
    if gil_status is None:
        return True
    return bool(gil_status())


def describe_runtime() -> dict[str, object]:
    return {
        "python_version": sys.version.split()[0],
        "free_threaded_build": is_free_threaded_build(),
        "gil_enabled": is_gil_enabled(),
    }


if __name__ == "__main__":
    for name, value in describe_runtime().items():
        print(f"{name}: {value}")

Jalankan pada dua interpreter:

python diagnostics.py
python3.14t diagnostics.py

Pada build free-threaded tanpa GIL, hasil yang diharapkan memuat free_threaded_build: True dan gil_enabled: False. Periksa output aktual di CI dan production karena PYTHON_GIL=1, -X gil=1, atau ekstensi yang tidak kompatibel dapat mengubah status runtime.

2. Audit dependency setelah import

Dukungan free-threading berlaku per package. Tracker komunitas mencantumkan contoh versi yang mendukung mode ini, antara lain NumPy 2.1, pandas 2.2.3, SQLAlchemy 2.0.45, cryptography 46.0.0, uvloop 0.22.1, aiohttp 3.13.0, dan tiktoken 0.12.0. Cocokkan versi tersebut dengan constraint aplikasi dan verifikasi environment yang sebenarnya.

Pemeriksaan harus dilakukan setelah dependency utama diimpor:

# verify_runtime.py
from __future__ import annotations

import sys
import sysconfig

import numpy
import sqlalchemy


def gil_is_enabled() -> bool:
    gil_status = getattr(sys, "_is_gil_enabled", None)
    if gil_status is None:
        return True
    return bool(gil_status())


def main() -> None:
    free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED") == 1
    print(f"free_threaded_build={free_threaded}")
    print(f"gil_enabled={gil_is_enabled()}")

    if not free_threaded:
        raise RuntimeError("Tes ini membutuhkan build free-threaded Python.")
    if gil_is_enabled():
        raise RuntimeError(
            "GIL aktif setelah import. Periksa PYTHON_GIL, -X gil=1, "
            "atau ekstensi yang mengaktifkan kembali GIL."
        )


if __name__ == "__main__":
    main()

Untuk setiap dependency, periksa ketersediaan wheel free-threaded, penggunaan ekstensi C, peringatan saat import, state global, dan hasil test suite pada kedua build. Import yang berhasil belum tentu berarti thread aplikasi dapat berjalan paralel.

3. Pisahkan fungsi CPU-bound dari request

Gunakan fungsi yang dapat diuji secara terpisah. Contoh ini sengaja memakai perulangan CPU murni sebagai stand-in preprocessing. Hasilnya bukan embedding production.

# app/services/chunk_processing.py
from __future__ import annotations


def embed_chunk(text: str) -> list[float]:
    normalized = text.casefold().strip()
    state = 0

    for round_number in range(12_000):
        for character in normalized:
            state = (state * 33 + ord(character) + round_number) % 1_000_003

    return [state / 1_000_003, len(normalized) / 10_000]

hashlib tidak cocok dijadikan bukti paralelisme Python karena beberapa operasi hash dapat melepaskan GIL. Gunakan workload nyata saat benchmark, bukan angka dari contoh ini.

4. Jalankan batch dengan ThreadPoolExecutor

Cache bersama membutuhkan lock. Lock tetap diperlukan pada build tanpa GIL, terutama untuk urutan baca, hitung, dan tulis.

# app/services/chunk_processing.py
from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor
from threading import Lock

_embedding_cache: dict[str, list[float]] = {}
_embedding_cache_lock = Lock()


def embed_chunk(text: str) -> list[float]:
    normalized = text.casefold().strip()
    state = 0
    for round_number in range(12_000):
        for character in normalized:
            state = (state * 33 + ord(character) + round_number) % 1_000_003
    return [state / 1_000_003, len(normalized) / 10_000]


def embed_chunk_cached(text: str) -> list[float]:
    with _embedding_cache_lock:
        cached = _embedding_cache.get(text)
    if cached is not None:
        return cached

    result = embed_chunk(text)
    with _embedding_cache_lock:
        return _embedding_cache.setdefault(text, result)


def embed_chunks(texts: list[str], workers: int) -> list[list[float]]:
    if workers < 1:
        raise ValueError("workers harus minimal 1")
    with ThreadPoolExecutor(
        max_workers=workers,
        thread_name_prefix="embedding",
    ) as executor:
        return list(executor.map(embed_chunk_cached, texts))

ThreadPoolExecutor tidak mengubah pekerjaan I/O-bound menjadi CPU-bound. Jangan menulis cache bersama tanpa perlindungan hanya karena operasi dictionary individual tampak aman pada build dengan GIL.

5. Integrasikan dengan management command Django

Pekerjaan batch biasanya lebih mudah diuji di luar request.

# app/management/commands/embed_documents.py
from __future__ import annotations

from pathlib import Path

from django.core.management.base import BaseCommand, CommandError
from django.db import close_old_connections

from app.services.chunk_processing import embed_chunks


class Command(BaseCommand):
    help = "Memproses chunk dokumen secara paralel."

    def add_arguments(self, parser) -> None:
        parser.add_argument("input_file", type=Path)
        parser.add_argument("--workers", type=int, default=4)

    def handle(self, *args, **options) -> None:
        input_file: Path = options["input_file"]
        workers: int = options["workers"]

        if workers < 1:
            raise CommandError("--workers harus minimal 1")
        if not input_file.is_file():
            raise CommandError(f"File tidak ditemukan: {input_file}")

        texts = [
            line.strip()
            for line in input_file.read_text(encoding="utf-8").splitlines()
            if line.strip()
        ]
        if not texts:
            self.stdout.write("Tidak ada chunk untuk diproses.")
            return

        close_old_connections()
        try:
            embeddings = embed_chunks(texts, workers=workers)
        finally:
            close_old_connections()

        self.stdout.write(
            self.style.SUCCESS(
                f"Memproses {len(embeddings)} chunk dengan {workers} worker."
            )
        )

Bandingkan interpreter dan konfigurasi worker dengan input yang sama:

python manage.py embed_documents chunks.txt --workers 1
python3.14t manage.py embed_documents chunks.txt --workers 4

Catat waktu, memori, error rate, dan hasil fungsional. Jangan membagikan objek koneksi database Django antar-thread.

6. Bedakan workload GraphQL

Satu request GraphQL dapat menjalankan banyak resolver, tetapi itu tidak otomatis berarti semua resolver cocok diparalelkan. Resolver CRUD yang menunggu database sebaiknya mendapat optimasi query lebih dulu. Batch CPU-bound seperti documentEmbeddings dapat memanggil service yang sama dengan management command.

# app/graphql/resolvers.py
from __future__ import annotations

from typing import Any

from app.services.chunk_processing import embed_chunks


def resolve_document_embeddings(
    root: Any,
    info: Any,
    chunks: list[str],
    workers: int = 1,
) -> list[list[float]]:
    if workers < 1:
        raise ValueError("workers harus minimal 1")
    return embed_chunks(chunks, workers=workers)

Tetapkan batas jumlah chunk, izin penggunaan worker, waktu maksimum request, dan format kegagalan sebagian. Batch panjang mungkin lebih tepat dipindahkan ke job queue daripada dijalankan synchronous di resolver.

7. Uji dua build di CI

Simpan test suite untuk Python biasa dan free-threaded. actions/setup-python mendukung pemilihan build melalui input freethreaded.

# .github/workflows/python.yml
name: Python tests

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: "3.14"
            python-version: "3.14"
            freethreaded: false
          - name: "3.14t"
            python-version: "3.14"
            freethreaded: true
    name: Python ${{ matrix.name }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}
          freethreaded: ${{ matrix.freethreaded }}
      - run: pip install -r requirements.txt
      - run: pip install -r requirements-dev.txt
      - run: python -m pytest

Tambahkan pemeriksaan runtime khusus pada job 3.14t, lalu jalankan seluruh suite pada kedua build. Test yang hanya lulus pada Python biasa belum cukup untuk mengaktifkan free-threading.

Checklist audit sebelum production

  • Py_GIL_DISABLED menunjukkan build yang diharapkan.
  • sys._is_gil_enabled() diperiksa setelah import dependency utama.
  • Tidak ada PYTHON_GIL=1 atau -X gil=1 yang aktif tanpa sengaja.
  • Ekstensi C penting diperiksa pada tracker free-threading.
  • Cache dan lazy initialization memiliki lock atau desain immutable.
  • Koneksi database tidak dibagikan sembarangan antar-thread.
  • close_old_connections() dipanggil sesuai pola pekerjaan.
  • Test Django dan GraphQL lulus pada kedua build.
  • Workload CPU-bound dipisahkan dari request I/O-bound.
  • Benchmark mengukur waktu, memori, error, dan hasil bisnis.
  • Ada rollback ke build Python dengan GIL.

Trade-off

Build free-threaded tetap opt-in

Python biasa masih menjadi default. Tim perlu menyediakan interpreter berbeda, menambah job CI, dan memelihara strategi rollback. Deployment, image container, atau tool internal mungkin belum mengenali build 3.14t.

Single-thread dapat memiliki biaya

Free-threading tidak gratis. Target Fase II mencakup regresi pyperformance paling banyak 15 persen dan overhead memori paling banyak 20 persen, sementara laporan yang dirangkum brief menyebut overhead single-thread sekitar 5 sampai 10 persen. Ukur aplikasi Django sendiri sebelum memilih jumlah worker.

Jika endpoint hampir seluruhnya menunggu database, optimasi query, batching database, caching, atau async I/O mungkin lebih relevan.

Kompatibilitas package tidak seragam

Package pure Python umumnya tidak memerlukan perubahan khusus. Package yang memakai ekstensi C perlu diperiksa satu per satu. Versi yang tercantum di tracker tetap harus cocok dengan dependency lain di aplikasi.

Shared state tidak lagi terlindungi secara kebetulan

Kode lama mungkin berjalan karena GIL mengurangi peluang akses Python berlangsung bersamaan. Audit penghitung, singleton, cache global, dan lazy initialization. Untuk state yang ditulis banyak thread, gunakan lock atau pindahkan state ke backend yang memang mendukung akses bersama.

Django dan GraphQL punya batas desain sendiri

Django 6.1 mendukung Python 3.12 sampai 3.14, tetapi itu bukan bukti bahwa semua konfigurasi free-threaded sudah didukung resmi. GraphQL juga tidak menghilangkan risiko terlalu banyak koneksi, beban database, dan request yang timeout.

Gunakan thread pool pada bagian yang jelas CPU-bound dan batasi jumlah worker berdasarkan pengukuran.

Kesimpulan dan CTA

Python 3.14t membuat free-threading menjadi opsi resmi yang layak diuji. Namun Python dengan GIL tetap default, dan build 3.14t bukan tombol percepatan untuk semua backend.

Mulai dari satu fungsi CPU-bound yang thread-friendly. Audit dependency, periksa status interpreter setelah import, lindungi shared state, kelola koneksi database, lalu uji Python 3.14 dan 3.14t di CI dengan input yang sama.

Pisahkan request CRUD dan resolver yang menunggu I/O dari batch preprocessing, embedding, pemrosesan dokumen, dan ETL. Biarkan hasil pengujian dan benchmark aplikasi yang menentukan apakah biaya kompatibilitas sepadan dengan manfaatnya.

Untuk bacaan lanjutan, mulai dari PEP 703, PEP 779, Python 3.14 What’s New, Free-threading HOWTO, tracker py-free-threading, Django 6.1 release notes, dan panduan instalasinya. Benchmark Miguel Grinberg dapat menjadi konteks, bukan pengganti pengukuran workload Anda sendiri.

↑