সরাসরি পর্বে যান
পর্ব ২৪ · ৫৯-এর মধ্যে · মডিউল ৪

নিজের package বানানো — pyproject, wheel আর PyPI

Packaging Your Own Code — pyproject.toml, Build Backends, Wheels & Publishing to PyPI

Taught in Bangla, this lesson shows how to turn your code into an installable package: src versus flat layout, every part of pyproject.toml including SPDX licenses and console scripts, build backends, building an sdist and a wheel and looking inside, PEP 440 versions, editable installs, testing in a fresh venv, and publishing via TestPyPI, tokens and trusted publishing.

এই পর্বে যা শিখবেন

  • src layout কেন ভুলবশত import-এর ফাঁদ আটকায়, আর একটি প্রকাশযোগ্য প্রজেক্টের কাঠামো
  • pyproject.toml-এর প্রতিটি অংশ — license (SPDX), classifier, optional dependency, console script
  • build backend বাছাই, python -m build চালানো আর wheel খুলে ভেতরটা পড়া
  • PEP 440 version, pre-release, এক জায়গায় version রাখা, আর library বনাম অ্যাপের dependency-নীতি
  • TestPyPI, twine, API token আর GitHub Actions থেকে trusted publishing; কোম্পানির private index

১ · ইনস্টল করা অর্ধেক গল্প — নিজের কোড কেন package হবে

নুসরাত ঢাকার একটি ই-কমার্স কোম্পানিতে Python ডেভেলপার। বছরখানেক আগে তিনি একটি ছোট ফাংশন লিখেছিলেন — format_taka(), যা 1234567-কে লেখে 12,34,567, অর্থাৎ বাংলাদেশের লাখ-কোটির নিয়মে কমা বসায়। ফাংশনটি এত কাজের যে সেটি এখন তিন জায়গায় আছে: ইনভয়েসের অ্যাপে, মাসিক বিক্রির রিপোর্ট-স্ক্রিপ্টে আর দোকানের ওয়েবসাইটে। তিনটিই কপি-পেস্ট করা।

একদিন ধরা পড়ল, ঋণাত্মক টাকায় (ফেরত দেওয়া অর্ডার) ফাংশনটি ভুল কমা বসায়। নুসরাত ইনভয়েস-অ্যাপে ঠিক করলেন। রিপোর্ট-স্ক্রিপ্ট আর ওয়েবসাইটে পুরোনো ভুল রয়ে গেল। এর মধ্যে এক সহকর্মী ওয়েবসাইট থেকে পুরোনো কপিটিই নিজের নতুন প্রজেক্টে নিয়ে গেছেন। এখন চারটি কপি, তিন রকম আচরণ, আর কেউ জানে না কোনটি “আসল”।

পর্ব ২৩-এ আপনি শিখেছেন অন্যের package কীভাবে আনতে হয় — pip, venv, version-শর্ত। কিন্তু সেটি গল্পের অর্ধেক। বাকি অর্ধেক হলো নিজের কোডকে এমন একটি জিনিসে রূপ দেওয়া, যা pip install করা যায়: একটি নাম, একটি version, নিজের dependency-তালিকা আর একটি মানক ফাইল-ফরম্যাট। তখন নুসরাতের সমাধান সহজ — taka-format নামে একটি package, version 0.1.0. ভুল ঠিক হলে 0.1.1, আর প্রতিটি প্রজেক্ট নিজের সুবিধামতো, পরীক্ষা করে আপগ্রেড করে। কোড থাকে এক জায়গায়, ইতিহাস থাকে version নম্বরে।

একটি মিষ্টির দোকানের কথা ভাবুন। বাড়িতে নিজের জন্য রসগোল্লা বানাতে রেসিপি মাথায় থাকলেই চলে। কিন্তু দোকানে বিক্রি করতে হলে লাগে প্যাকেট, প্যাকেটের গায়ে নাম, ওজন, উপাদানের তালিকা, তৈরির তারিখ (version) আর একটি নির্দিষ্ট মাপের বাক্স, যা যেকোনো ডেলিভারি-ভ্যানে ধরে। Python-এ রেসিপি হলো আপনার কোড, প্যাকেটের লেবেল pyproject.toml, বাক্স হলো wheel, আর বড় পাইকারি বাজার PyPI.

এই একই পথে তিনটি ভিন্ন গন্তব্যে যাওয়া যায়:

🧑‍💻 নিজের প্রজেক্টগুলো

একই utility কোড পাঁচটি প্রজেক্টে কপি না করে একটি package. git থেকে বা নিজের ফোল্ডারের wheel থেকে ইনস্টল।

pip install ./dist/*.whl

🏢 দল বা কোম্পানি

ভেতরের লাইব্রেরি (যেমন কোম্পানির পেমেন্ট-গেটওয়ের ক্লায়েন্ট) শুধু কর্মীদের জন্য — private package index-এ।

অংশ ৯

🌍 সারা পৃথিবী

PyPI-তে প্রকাশ — যে কেউ pip install taka-format দিতে পারবেন। requests, NumPy, Django সবাই এভাবেই এসেছে।

অংশ ৮

আধুনিক Python-এ পুরো কাজটি ঘোষণামূলক: আপনি একটি ফাইলে লিখে দেন প্রজেক্টটি কী — কোনো স্ক্রিপ্ট লিখে বলতে হয় না কীভাবে বানাতে হবে। তারপর একটি build backend সেই ঘোষণা পড়ে দুটি মানক ফাইল বানায়, যা pip, uv বা যেকোনো ইনস্টলার বোঝে। পুরো যাত্রাটি এক নজরে:

কোড থেকে অন্যের কম্পিউটার পর্যন্ত

source → declarative metadata → standard artifacts → index → install

  1. কোডsrc/taka_format/আপনার module
  2. লেবেলpyproject.tomlনাম, version, dependency
  3. Buildpython -m buildbackend কাজ করে
  4. দুটি ফাইল.whl + .tar.gzdist/ ফোল্ডারে
  5. IndexPyPI / privatepip install
ছবি ২৪.১ — একটি package-এর জীবনচক্র। এই পর্বের প্রতিটি অংশ এই শিকলের একটি কড়া।

দুটি শব্দ মনে করিয়ে দিই (পর্ব ২২ ও ২৩)। import package হলো __init__.py-সহ একটি ফোল্ডার, কোডে যার নাম লেখেন (import taka_format). আর distribution হলো সেই ফোল্ডার-সহ নাম ও version লাগানো বান্ডিল, যা pip ইনস্টল করে (pip install taka-format). এই পর্বের পুরোটা দ্বিতীয়টি বানানোর গল্প। একটি পরিচিত নিয়ম: distribution-এর নামে সাধারণত হাইফেন, import-এর নামে আন্ডারস্কোর, কারণ Python-এর নামে হাইফেন চলে না।

C/C++ জানলে

C/C++-এ library ভাগ করা মানে সাধারণত header আর compile করা .so/.dll/.a ফাইল দেওয়া, আর প্রতিটি OS ও compiler-এর জন্য আলাদা build — কোনো একক মানক ফরম্যাট বা কেন্দ্রীয় ভাণ্ডার নেই (vcpkg, Conan আছে, তবে সর্বজনীন নয়)। বিশুদ্ধ Python package-এ compile নেই, তাই একটি wheel সব OS-এ চলে; কিন্তু ভেতরে C extension থাকলে Python-কেও প্রতি প্ল্যাটফর্মে আলাদা wheel বানাতে হয় — ঠিক C++-এর সেই সমস্যা। তুলনার জন্য C++ কোর্সের CMake ও build পর্ব দেখুন; Java জানলে Maven Central-এ প্রকাশের ধারণা প্রায় একই — Java কোর্সের Maven ও Gradle পর্ব.

২ · প্রজেক্ট সাজানো — src layout বনাম flat layout

নুসরাতের package-এর পুরো ফোল্ডার নিচে দেখানো হলো। এই পর্ব লেখার আগে আমরা ঠিক এই প্রজেক্টটি সত্যিই বানিয়ে, build করে, আলাদা venv-এ ইনস্টল করে চালিয়ে দেখেছি — সামনের সব আউটপুট সেখান থেকেই নেওয়া।

taka-format/
├── pyproject.toml          ← পরিচয়পত্র (অংশ ৩)
├── README.md               ← PyPI-র পাতায় দেখাবে
├── LICENSE                 ← ব্যবহারের অনুমতির শর্ত
├── src/
│   └── taka_format/        ← import করা যায় এমন package
│       ├── __init__.py     ← format_taka()
│       ├── cli.py          ← terminal-এর "taka" কমান্ড
│       └── units.txt       ← package-এর সাথে যাওয়া ডেটা-ফাইল
└── tests/
    └── test_format.py

পর্ব ২৩-এ src layout-এর নাম শুনেছেন। এর বিকল্প হলো flat layout, যেখানে taka_format/ ফোল্ডার সরাসরি প্রজেক্টের মূলে থাকে, src/ ছাড়া। দুটিই বৈধ, দুটিই প্রচলিত — Python Packaging User Guide দুটির তুলনাই দেয়। তবে প্রকাশ করার মতো package-এ src layout বেশি নিরাপদ, আর তার কারণ একটি সূক্ষ্ম ফাঁদ।

পর্ব ২২-এ দেখেছেন, কোনো স্ক্রিপ্ট চালালে তার ফোল্ডার (আর python -m বা REPL-এ বর্তমান ফোল্ডার) sys.path-এর একেবারে শুরুতে বসে। Flat layout-এ প্রজেক্টের মূল থেকে টেস্ট চালালে import taka_format তাই ইনস্টল করা package নয়, পাশের ফোল্ডারটিই পেয়ে যায়। ফলে wheel-এ কোনো ফাইল বাদ পড়লেও আপনার কম্পিউটারে সব ঠিকঠাক চলে — ভাঙে শুধু ব্যবহারকারীর কম্পিউটারে। ধাপে ধাপে দেখুন:

  1. Flat./taka_format/মূল ফোল্ডারে
  2. অসম্পূর্ণ wheelunits.txt নেইকেউ টের পায়নি
  3. টেস্ট পাসsys.path[0] = .ভুল জিনিস পরীক্ষা
  4. ব্যবহারকারীFileNotFoundErrorপ্রকাশের পর
  5. src layoutsrc/taka_format/ইনস্টল ছাড়া import নেই
Flat layoutpackage মূল ফোল্ডারে
taka-format/
├── pyproject.toml
├── taka_format/       ← সরাসরি এখানে
│   ├── __init__.py
│   └── units.txt
└── tests/
wheel-এর ভেতরেbuild-এর সেটিং-এ একটি ভুল
taka_format/__init__.py
taka_format/cli.py
taka_format/units.txt      ← নেই!
taka_format-0.1.0.dist-info/...
নুসরাতের terminalপ্রজেক্টের মূল থেকে
$ python -m pytest
sys.path[0] = taka-format/   → import taka_format
            = পাশের ফোল্ডার, যেখানে units.txt আছে
1 passed
ব্যবহারকারীর terminalPyPI থেকে ইনস্টলের পর
$ taka
FileNotFoundError: ... taka_format/units.txt
ইনস্টল করা wheel-এ ফাইলটি কখনো ছিলই না
src layoutপ্রজেক্টের মূল থেকে, ইনস্টল ছাড়া
$ python -c "import taka_format"
ModuleNotFoundError: No module named 'taka_format'
→ আগে ইনস্টল করতেই হবে; টেস্ট চলে ইনস্টল করা কপিতে
  1. Flat layout. package-এর ফোল্ডার প্রজেক্টের মূলে, pyproject.toml-এর পাশে। দেখতে সরল, ছোট প্রজেক্টে কোনো সমস্যাও হয় না। কিন্তু মূল ফোল্ডার থেকে কিছু চালালেই এই ফোল্ডার “import করা যায়” — ইনস্টল না করেও।
  2. wheel-এ একটি ফাইল বাদ। ধরুন build-এর সেটিং-এ একটি ভুলে units.txt wheel-এ ঢোকেনি (অংশ ৭-এ দেখবেন, ডেটা-ফাইল নেওয়ার নিয়ম backend ভেদে আলাদা)। build কোনো error দেয় না — সে যা বলা হয়েছে, তা-ই করেছে।
  3. তবু টেস্ট পাস। নুসরাত মূল ফোল্ডার থেকে pytest চালালেন। sys.path-এর শুরুতে মূল ফোল্ডার, তাই import taka_format পেল পাশের ফোল্ডারটি — যেখানে units.txt দিব্যি আছে। টেস্টগুলো আসলে wheel পরীক্ষাই করেনি।
  4. প্রকাশের পর ভাঙন। ব্যবহারকারী PyPI থেকে সেই অসম্পূর্ণ wheel পেলেন, আর প্রথম চালানোতেই FileNotFoundError. সবচেয়ে খারাপ সময়ে ধরা পড়ল — প্রকাশের পরে, অন্যের কম্পিউটারে। আর একবার প্রকাশিত version বদলানোও যায় না (অংশ ৬)।
  5. src layout ফাঁদটি বন্ধ করে। package src/-এর ভেতরে থাকায় মূল ফোল্ডারটি আর import-যোগ্য নয়। ইনস্টল না করলে ModuleNotFoundError — আমরা নিজেরা চালিয়ে ঠিক এই বার্তাই পেয়েছি। তাই টেস্টকে বাধ্য হয়ে ইনস্টল করা কপিতে চলতে হয়, আর ব্যবহারকারী যা পাবেন, আপনিও তা-ই পরীক্ষা করেন।

এখন package-এর আসল কোড। নিচের ফাংশনটি src/taka_format/__init__.py-এর হৃদয় (সেখানে বাড়তি শুধু version পড়ার দুটি লাইন, অংশ ৬-এ দেখবেন)। লাখ-কোটির নিয়ম: শেষ তিন অঙ্ক একটি দল, তারপর বাঁ দিকে দুই অঙ্ক করে। প্রকাশের আগে package-এর আচরণ টেস্ট দিয়ে বেঁধে ফেলা জরুরি — নুসরাতের ঋণাত্মক সংখ্যার বাগটিও এখানে একটি টেস্ট:

def format_taka(amount: int) -> str:
    """1234567 -> "12,34,567": শেষে ৩ অঙ্ক, তারপর ২ অঙ্ক করে দল।"""
    sign = "-" if amount < 0 else ""
    digits = str(abs(amount))
    head, tail = digits[:-3], digits[-3:]
    groups = []
    while head:
        groups.insert(0, head[-2:])
        head = head[:-2]
    return sign + ",".join(groups + [tail])

tests = {0: "0", 999: "999", 1000: "1,000", 100000: "1,00,000",
         1234567: "12,34,567", 10000000: "1,00,00,000", -5000: "-5,000"}
for n, want in tests.items():
    got = format_taka(n)
    assert got == want, f"{n}: পেলাম {got!r}, চাই {want!r}"
    print(f"{n:>9} -> {got}")
print("৭টি টেস্টই পাস")

চিহ্ন আলাদা করে রেখে (sign) শুধু অঙ্কগুলো নিয়ে কাজ — ঋণাত্মক সংখ্যার বাগ এভাবেই সারে। digits[-3:] শেষ তিন অঙ্ক; বাকি head থেকে লুপে শেষের দুই অঙ্ক করে কেটে সামনে বসানো হয়। ছোট সংখ্যায় (999) head ফাঁকা, তাই লুপ চলেই না। আউটপুট সাত লাইন (বাঁয়ের সংখ্যা :>9-এর কারণে ৯ ঘরে ডানদিক ঘেঁষা): 0 -> 0, 999 -> 999, 1000 -> 1,000, 100000 -> 1,00,000, 1234567 -> 12,34,567, 10000000 -> 1,00,00,000, -5000 -> -5,000, তারপর “৭টি টেস্টই পাস”। কোনো assert ব্যর্থ হলে প্রোগ্রাম AssertionError দিয়ে থামত। বাস্তব প্রজেক্টে এই টেস্টগুলো থাকে tests/ ফোল্ডারে, pytest দিয়ে চলে (পর্ব ৪২)।

মূল ধারণা

নাম বাছার আগে PyPI দেখুন। PyPI-তে নাম আগে এলে আগে পাবেন, আর নাম তুলনার সময় বড়-ছোট হাতের অক্ষর, -, _ আর . একই ধরা হয় (PEP 503-এর normalization) — তাই Taka_Format আর taka-format একই নাম। আমাদের build-এর ফাইলের নামেও তাই taka_format-0.1.0... দেখবেন, যদিও প্রজেক্টের নাম taka-format.

৩ · pyproject.toml — পরিচয়পত্রের প্রতিটি ঘর

পর্ব ২৩-এ একটি ছোট pyproject.toml দেখেছেন — নাম, version, dependency আর একটি কমান্ড। প্রকাশ করার মতো package-এ আরও কিছু ঘর লাগে, কারণ এগুলোই PyPI-র পাতায় দেখা যায়, আর ব্যবহারকারী ও টুল এগুলো পড়েই সিদ্ধান্ত নেয়। নিচেরটি আমাদের পরীক্ষিত taka-format-এর হুবহু ফাইল:

# pyproject.toml
[build-system]
requires = ["hatchling>=1.27"]
build-backend = "hatchling.build"

[project]
name = "taka-format"
version = "0.1.0"
description = "Format numbers in Bangladeshi lakh/crore style"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "Nusrat Jahan", email = "nusrat@example.com" }]
dependencies = []
classifiers = [
    "Programming Language :: Python :: 3",
    "Operating System :: OS Independent",
]

[project.optional-dependencies]
cli = ["rich>=13"]

[project.urls]
Source = "https://github.com/example/taka-format"

[project.scripts]
taka = "taka_format.cli:main"
ঘরকী বলেখেয়াল রাখুন
[build-system]কোন backend build করবে, আর তার জন্য কী ইনস্টল লাগবেbackend-এর ন্যূনতম version দিন, যদি নতুন কোনো সুবিধা লাগে (এখানে license-এর নতুন নিয়ম)
name, versionPyPI-তে পরিচয়একই নাম + version দ্বিতীয়বার আপলোড করা যায় না
description, readmeএক লাইনের সারাংশ আর PyPI-র পুরো পাতাREADME-ই আপনার package-এর দোকানের সাইনবোর্ড
requires-pythonকোন Python-এ চলেপুরোনো Python-এ pip এই version বাদ দিয়ে আগের উপযুক্ত version খোঁজে
license, license-filesব্যবহারের অনুমতি, SPDX ভাষায়নিচে বিস্তারিত — এখানেই AI সবচেয়ে বেশি পুরোনো রূপ লেখে
dependenciespackage চলতে যা লাগবেইlibrary-তে ঢিলে পরিসর, == নয় (অংশ ৬)
optional-dependenciesঐচ্ছিক “extra”pip install "taka-format[cli]" দিলে rich-ও আসে
classifiersPyPI-র খোঁজ-ফিল্টারের মানক লেবেলতালিকার বাইরের লেবেল PyPI আপলোডের সময় ফিরিয়ে দেয়
[project.urls]source, ডকুমেন্টেশন, বাগ-রিপোর্টের ঠিকানাব্যবহারকারী এগুলো দেখেই বিশ্বাস করেন (পর্ব ২৩-এর যাচাই)
[project.scripts]terminal-কমান্ড → ফাংশনইনস্টলের সময় pip একটি ছোট চালানোর ফাইল বানায়

License — SPDX expression (PEP 639)

অনেক বছর ধরে license লেখার কোনো একক নিয়ম ছিল না: কেউ লিখতেন license = {text = "MIT"}, কেউ classifier দিতেন "License :: OSI Approved :: MIT License", কেউ দুটোই — আর মাঝে মাঝে দুটি আলাদা কথা বলত। PEP 639 (২০২৪-২৫ সালে গৃহীত) একটি পরিষ্কার নিয়ম আনে: license-এ একটি SPDX license expression, যেমন "MIT", "Apache-2.0" বা দুটির যেকোনো একটি বেছে নেওয়ার জন্য "MIT OR Apache-2.0". আর license-files-এ license-এর আসল লেখার ফাইল, যা wheel-এর ভেতরে যায় — আমাদের wheel-এ সেটি বসেছে .dist-info/licenses/LICENSE-এ।

  • backend-কে নতুন হতে হবে: hatchling 1.27 আর setuptools 77 থেকে এই রূপ সমর্থিত। তাই আমাদের ফাইলে hatchling>=1.27. আমাদের build-এর METADATA-তে এসেছে License-Expression: MIT.
  • পুরোনো রূপ এড়ান: license = {text = ...} টেবিল-রূপ এখন বাতিলের পথে, আর License :: classifier SPDX expression-এর সাথে একসাথে দিলে নতুন টুল error দিতে পারে। একটিই সত্যের উৎস — license.
  • license না দিলে: আইনের চোখে কোড ব্যবহারের অনুমতি অস্পষ্ট থাকে, আর সাবধানী কোম্পানি সেই package ব্যবহারই করে না। ছোট utility-তে MIT বা Apache-2.0 সবচেয়ে প্রচলিত; কোনটি বাছবেন, সেটি আইনি সিদ্ধান্ত — কোম্পানির কোড হলে আগে অনুমতি নিন।

Classifier, extra আর এমন জিনিস যা প্রকাশ হয় না

Classifier হলো PyPI-র নির্দিষ্ট তালিকা থেকে নেওয়া লেবেল — কোন Python, কোন OS, প্রজেক্টের পরিপক্কতা (Development Status :: 4 - Beta). এগুলো খোঁজায় সাহায্য করে, কিন্তু ইনস্টলের নিয়ম বদলায় না — Python version-এর আসল নিয়ন্ত্রণ requires-python. একটি কাজের কৌশল: Private :: দিয়ে শুরু হওয়া যেকোনো classifier (যেমন "Private :: Do Not Upload") PyPI ইচ্ছাকৃতভাবে ফিরিয়ে দেয়। কোম্পানির ভেতরের package-এ এটি দিলে ভুল করে পাবলিক PyPI-তে আপলোড হওয়া আটকায়।

Optional dependencies (extra) দিয়ে ভারী বা বিশেষ dependency ঐচ্ছিক রাখা যায়। আমাদের METADATA-তে এটি দেখা গেছে এভাবে: Provides-Extra: cli আর Requires-Dist: rich>=13; extra == 'cli' — অর্থাৎ rich আসবে শুধু [cli] চাইলে। আর যেসব dependency শুধু ডেভেলপারের লাগে (pytest, ruff), সেগুলো ব্যবহারকারীর কাছে যাওয়া উচিত নয়। এর জন্য নতুন মানক [dependency-groups] অংশ (PEP 735, ২০২৪) — এটি wheel-এর metadata-তে যায় না; uv এটি পড়ে, আর pip 25.1 থেকে --group দিয়ে। টুলের নিজস্ব সেটিং থাকে [tool.ruff], [tool.pytest.ini_options]-এর মতো [tool.*] অংশে (পর্ব ৪২, ৪৭)।

৪ · Build backend বাছাই — frontend আর backend-এর ভাগাভাগি

package বানানোর কাজে দুই ধরনের টুল আছে, আর এদের ভাগটা বুঝলে বাকি সব সহজ হয়ে যায়। Build frontend হলো যে টুল আপনি চালান — python -m build, pip বা uv build. এটি নিজে কিছু বানায় না। সে [build-system] পড়ে, একটি অস্থায়ী, আলাদা পরিবেশ তৈরি করে, সেখানে backend ইনস্টল করে, তারপর PEP 517-এর মানক ফাংশন (build_wheel, build_sdist) ডেকে backend-কে বলে “বানাও”। এই আলাদা পরিবেশকে বলে build isolation — আপনার venv-এ কী আছে না আছে, build তার ওপর নির্ভর করে না, তাই সবার কম্পিউটারে একই ফল।

Backend হলো যে আসলে ফাইল বানায়। কোনটি বাছবেন? সত্যি কথা হলো, বিশুদ্ধ Python package-এর জন্য সবগুলোই ভালো, আর একই [project] অংশ সবাই পড়ে — backend বদলাতে সাধারণত শুধু [build-system]-এর দুটি লাইন আর backend-নির্ভর [tool.*] সেটিং বদলাতে হয়।

Backendbuild-backendবৈশিষ্ট্যকখন ভাববেন
setuptoolssetuptools.build_metaসবচেয়ে পুরোনো (২০০৪ থেকে), বিশাল ইকোসিস্টেম, C extension build করতে পারে; ৬১.০ (২০২২) থেকে [project] পড়েপুরোনো প্রজেক্ট, C extension, setup.py থেকে ধীরে সরে আসা
hatchlinghatchling.buildHatch প্রজেক্টের backend, PyPA-র ছাতার নিচে; ভালো ডিফল্ট, plugin দিয়ে বাড়ানো যায়নতুন বিশুদ্ধ Python প্রজেক্ট (এই পর্বের উদাহরণ)
flit-coreflit_core.buildapiঅত্যন্ত ছোট ও সরল, শুধু বিশুদ্ধ Python, কনফিগ প্রায় লাগে নাছোট library, কম নির্ভরতা চাইলে
pdm-backendpdm.backendPDM প্রজেক্টের backend, PDM ছাড়াও একা ব্যবহার করা যায়PDM ব্যবহারকারী দল
uv_builduv_buildAstral-এর uv-এর সাথে আসা দ্রুত backend (২০২৫ সালে স্থিতিশীল), শুধু বিশুদ্ধ Pythonuv দিয়ে চলা প্রজেক্ট; uv-এর নতুন version-এ uv init --package এটিই বসায়

ভেতরে C, C++ বা Rust কোড থাকলে আলাদা জাতের backend লাগে — যেমন CMake-ভিত্তিক scikit-build-core বা Rust-এর জন্য maturin. version নম্বরগুলো এই লেখার সময়ের; backend দ্রুত বদলায়, তাই নতুন প্রজেক্ট শুরুর সময় সংশ্লিষ্ট ডকুমেন্টেশন একবার দেখে নিন।

setup.py কি মরে গেছে? না, তবে তার ভূমিকা বদলেছে। python setup.py install বা setup.py sdist upload-এর মতো সরাসরি কমান্ড বহু আগেই বাতিলের পথে। setuptools এখনো setup.py পড়তে পারে — জটিল C build-এর কিছু কাজের জন্য সেটি রাখা যায় — কিন্তু নাম, version, dependency-র মতো পরিচয় এখন [project]-এ লেখাই মানক। AI যদি নতুন প্রজেক্টের জন্য শুধু setup.py লিখে দেয়, বুঝবেন উত্তরটি পুরোনো যুগের (অংশ ১০)।

৫ · Build করা — আর wheel-এর ভেতরে আসলে কী থাকে

প্রজেক্টের মূল ফোল্ডারে একটি আলাদা venv-এ build নামের frontend ইনস্টল করে একটি কমান্ড দিন:

python -m venv .venv
.venv\Scripts\Activate.ps1          # Mac/Linux: source .venv/bin/activate
python -m pip install build
python -m build                     # dist/ ফোল্ডারে sdist আর wheel

প্রত্যাশিত আউটপুট (সংক্ষেপে) — আমাদের নিজের চালানো থেকে, build 1.6.1 দিয়ে:

* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling>=1.27
* Installed build dependency versions:
  - hatchling==1.32.4
* Building sdist...
* Building wheel from sdist
...
Successfully built taka_format-0.1.0.tar.gz and taka_format-0.1.0-py3-none-any.whl

লক্ষ করুন Building wheel from sdist: build প্রথমে sdist বানায়, তারপর সেই sdist খুলে তা থেকে wheel বানায়। এটি একটি চমৎকার পরীক্ষা — sdist-এ কোনো দরকারি ফাইল বাদ পড়লে wheel build এখানেই ব্যর্থ হবে, ব্যবহারকারীর কম্পিউটারে নয়। পুরো যাত্রাটি, ইনস্টল পর্যন্ত:

  1. পড়া[build-system]frontend
  2. Backendbuild_sdisthatchling
  3. wheelzip + dist-infodist/
  4. pip installsite-packagesখুলে বসানো
  5. কমান্ডtakaScripts/bin-এ
frontend পড়েpyproject.toml
[build-system]
requires = ["hatchling>=1.27"]
build-backend = "hatchling.build"
→ অস্থায়ী venv বানাও, hatchling ইনস্টল করো
sdist-এর ভেতরেtaka_format-0.1.0.tar.gz
taka_format-0.1.0/pyproject.toml
taka_format-0.1.0/README.md
taka_format-0.1.0/LICENSE
taka_format-0.1.0/src/taka_format/__init__.py
taka_format-0.1.0/src/taka_format/cli.py
taka_format-0.1.0/src/taka_format/units.txt
taka_format-0.1.0/tests/test_format.py
taka_format-0.1.0/PKG-INFO    ← metadata
wheel-এর ভেতরে (বাইট)taka_format-0.1.0-py3-none-any.whl
   416  taka_format/__init__.py
   357  taka_format/cli.py
    36  taka_format/units.txt
   575  taka_format-0.1.0.dist-info/METADATA
    87  taka_format-0.1.0.dist-info/WHEEL
    46  taka_format-0.1.0.dist-info/entry_points.txt
   141  taka_format-0.1.0.dist-info/licenses/LICENSE
   645  taka_format-0.1.0.dist-info/RECORD
নতুন venv-এ ইনস্টলপ্রত্যাশিত আউটপুট (সংক্ষেপে)
$ python -m pip install dist/taka_format-0.1.0-py3-none-any.whl
Processing .\dist\taka_format-0.1.0-py3-none-any.whl
Installing collected packages: taka-format
Successfully installed taka-format-0.1.0
site-packages/taka_format/  +  taka_format-0.1.0.dist-info/
Terminalপ্রত্যাশিত আউটপুট
$ taka 1234567 -5000 999
৳ 12,34,567
৳ -5,000
৳ 999
Windows-এ .venv\Scripts\taka.exe, Linux/Mac-এ .venv/bin/taka
  1. frontend pyproject পড়ে। python -m build প্রথমে শুধু [build-system] দেখে: কোন backend, তার কী লাগবে। তারপর একটি অস্থায়ী, পরিষ্কার পরিবেশ বানিয়ে সেখানে hatchling ইনস্টল করে। আপনার venv-এ hatchling না থাকলেও চলে — build isolation.
  2. backend sdist বানায়। frontend backend-এর build_sdist ডাকে। hatchling [project] পড়ে metadata লেখে (PKG-INFO), আর source, README, LICENSE, tests একটি .tar.gz-এ ভরে। sdist হলো “রান্নার আগের উপকরণ” — যাঁর কম্পিউটারে মিলের wheel নেই, তিনি এটি থেকে নিজে build করবেন।
  3. wheel — একটি zip. sdist থেকে build_wheel. wheel-এ tests বা pyproject থাকে না; থাকে শুধু ইনস্টলের জিনিস: package-এর ফাইল আর .dist-info ফোল্ডার — METADATA (নাম, version, dependency), WHEEL (কে বানাল, কোন ট্যাগ), entry_points.txt (কমান্ড), license আর RECORD (প্রতিটি ফাইলের hash).
  4. pip খুলে বসায়। ইনস্টল মানে মূলত unzip: ফাইলগুলো চালু venv-এর site-packages-এ, পাশে .dist-info, আর pip নিজে যোগ করে INSTALLER ফাইল (ভেতরে লেখা pip). কোনো কোড চালানো হয় না — wheel-এর বড় সুবিধা। pip uninstall পরে RECORD দেখে ঠিক এই ফাইলগুলোই মোছে।
  5. কমান্ড তৈরি। entry_points.txt-এ লেখা taka = taka_format.cli:main দেখে pip venv-এর Scripts (Windows-এ taka.exe) বা bin ফোল্ডারে একটি ছোট লঞ্চার বানায়, যা main() ডাকে। venv activate থাকলে শুধু taka লিখলেই চলে — আমাদের নতুন venv-এ ঠিক এই আউটপুট এসেছে।

wheel খুলে দেখা — শুধু standard library দিয়ে

wheel যেহেতু সাধারণ zip, Python-এর zipfile দিয়েই খোলা যায়, আর METADATA ফাইলটি ইমেইলের হেডারের মতো “নাম: মান” ফরম্যাটে লেখা — তাই email.parser দিয়ে পড়া যায়। এই পাতায় আসল ফাইল নেই, তাই নিচের কোড মেমরিতে একটি ছোট “wheel” বানিয়ে তারপর সেটি পড়ে:

import io
import zipfile
from email.parser import Parser

DI = "taka_format-0.1.0.dist-info"
buf = io.BytesIO()                       # ডিস্কে নয়, মেমরিতে একটি "wheel"
with zipfile.ZipFile(buf, "w") as whl:
    whl.writestr("taka_format/__init__.py", "def format_taka(n): ...\n")
    whl.writestr(f"{DI}/METADATA", "Metadata-Version: 2.4\nName: taka-format\n"
                 "Version: 0.1.0\nLicense-Expression: MIT\nRequires-Python: >=3.10\n")
    whl.writestr(f"{DI}/entry_points.txt", "[console_scripts]\ntaka = taka_format.cli:main\n")

with zipfile.ZipFile(buf) as whl:
    print(*whl.namelist(), sep="\n")
    meta = Parser().parsestr(whl.read(f"{DI}/METADATA").decode())
    print("নাম:", meta["Name"], "| version:", meta["Version"], "| license:", meta["License-Expression"])

name, ver, py, abi, plat = "taka_format-0.1.0-py3-none-any.whl".removesuffix(".whl").split("-")
print(f"Python ট্যাগ={py}, ABI={abi}, প্ল্যাটফর্ম={plat}")

প্রথম অংশ io.BytesIO-তে (মেমরির একটি “ফাইল”) তিনটি ফাইলসহ একটি zip লেখে — ডিস্কে কিছুই যায় না। দ্বিতীয় অংশ সেটি আবার খুলে তিনটি নাম ছাপে: taka_format/__init__.py, taka_format-0.1.0.dist-info/METADATA, taka_format-0.1.0.dist-info/entry_points.txt. তারপর METADATA পড়ে লাইন আসে নাম: taka-format | version: 0.1.0 | license: MIT. শেষ লাইনে ফাইলের নাম ভেঙে Python ট্যাগ=py3, ABI=none, প্ল্যাটফর্ম=any. নিজের কম্পিউটারে আসল wheel-এ একই কাজ করতে শুধু buf-এর জায়গায় ফাইলের পথ দিন — আমরা ঠিক এভাবেই ওপরের তালিকা পেয়েছি। আসল hatchling 1.32.4 লিখেছে Metadata-Version: 2.5; এখানে 2.4 রাখা হয়েছে, কারণ License-Expression ঘরটি সেই version-এ এসেছে।

wheel-এর নামের ট্যাগ — কে কোথায় চলবে

wheel-এর নামের গঠন: {নাম}-{version}-{python}-{abi}-{প্ল্যাটফর্ম}.whl. এই শেষ তিনটি অংশকে বলে wheel tag, আর pip এগুলো মিলিয়েই ঠিক করে কোন ফাইল আপনার কম্পিউটারে চলবে:

ট্যাগমানে
py3-none-anyযেকোনো Python 3, কোনো compile করা অংশ নেই, যেকোনো OS — pure-Python wheel. একটি ফাইলই সবার জন্য।
cp313-cp313-win_amd64শুধু CPython 3.13, তার নিজস্ব binary interface, 64-বিট Windows — C extension-সহ platform wheel.
cp313-cp313-manylinux_2_28_x86_64অনেক Linux distribution-এ চলে এমন মানক (glibc 2.28 বা নতুন)।
cp310-abi3-macosx_11_0_arm64CPython 3.10 বা নতুন সবগুলোতে চলে (stable ABI), Apple Silicon Mac.

platform wheel বানানো ঝামেলার — প্রতিটি OS, প্রসেসর আর Python version-এর জন্য আলাদা। বড় প্রজেক্টগুলো এজন্য cibuildwheel-এর মতো টুল দিয়ে CI-তে একসাথে ডজনখানেক wheel বানায়। আমাদের taka-format-এর মতো বিশুদ্ধ Python-এ সেই চিন্তা নেই।

python -m pip install twine
twine check dist/*        # README ঠিকমতো দেখাবে কি না, metadata বৈধ কি না

প্রত্যাশিত আউটপুট (সংক্ষেপে) — আমাদের চালানোয় twine 7.0.0:

Checking dist/taka_format-0.1.0-py3-none-any.whl: PASSED
Checking dist/taka_format-0.1.0.tar.gz: PASSED

৬ · Version — PEP 440, pre-release আর এক জায়গায় version

পর্ব ২৩-এ semantic versioning-এর প্রথা দেখেছেন: MAJOR.MINOR.PATCH. ব্যবহারকারী হিসেবে সেটি ছিল “পড়ার” বিষয়; package-এর লেখক হিসেবে এখন এটি আপনার প্রতিশ্রুতি। taka-format-এ format_taka()-এর নাম বদলালে MAJOR বাড়াতে হবে, নতুন ফাংশন যোগ করলে MINOR, শুধু বাগ সারালে PATCH. 0.x version-এর প্রচলিত মানে “এখনো স্থির নয়, MINOR-এও বড় বদল হতে পারে” — তাই প্রথম প্রকাশ প্রায়ই 0.1.0.

Python-এ কোন version লেখা বৈধ আর কোনটি কার চেয়ে বড়, তার আসল নিয়ম PEP 440-এ। semver-এর চেয়ে এটি একটু আলাদা, বিশেষ করে pre-release-এ:

1.0.0.dev1 < 1.0.0a1 < 1.0.0b2 < 1.0.0rc1 < 1.0.0 < 1.0.0.post1
  • .devN — ডেভেলপমেন্ট চলাকালীন build; aN / bN / rcN — alpha, beta, release candidate. pip এগুলো নিজে থেকে বাছে না: pip install taka-format সবচেয়ে নতুন স্থিতিশীল version নেয়। pre-release চাইলে --pre দিতে হয়, বা শর্তেই pre-release লিখতে হয় (taka-format==1.0.0rc1).
  • .postN — কোড না বদলে শুধু প্রকাশে ভুল সারানো (যেমন README). কোডের বাগ সারালে PATCH বাড়ান, post নয়।
  • একবার আপলোড, চিরকাল সেই ফাইল: PyPI একই নাম-version-এর ফাইল দ্বিতীয়বার নেয় না — মুছে দিলেও না। ভুল প্রকাশ হলে নতুন version দিন। খারাপ version-কে “yank” (PEP 592) করা যায়: ফাইল থাকে, কিন্তু শর্ত ছাড়া ইনস্টলে pip সেটি আর বাছে না; যাঁরা == দিয়ে হুবহু চেয়েছেন, শুধু তাঁরা পান।

version এক জায়গায় — importlib.metadata

নতুনরা প্রায়ই version দুই জায়গায় লেখেন — pyproject.toml-এ আর __init__.py-এর __version__-এ। তারপর একটিতে বদলান, অন্যটিতে ভুলে যান। সমাধান: version লিখুন শুধু pyproject.toml-এ, আর কোডে ইনস্টল করা metadata থেকে পড়ুন। আমাদের __init__.py-এর শুরু:

from importlib.metadata import version

__version__ = version("taka-format")   # distribution-এর নাম, import-এর নয়

importlib.metadata (Python 3.8 থেকে standard library-তে) site-packages-এর .dist-info/METADATA পড়ে। আমাদের নতুন venv-এ version('taka-format') দিয়েছে 0.1.0, আর একই module থেকে metadata('taka-format')['License-Expression'] দিয়েছে MIT. একটি শর্ত: package ইনস্টল করা থাকতে হবে (editable হলেও চলে) — src layout-এ এমনিতেই তা থাকে। বিকল্প পথও আছে: backend-কে dynamic = ["version"] দিয়ে বলা যায় version কোডের ফাইল থেকে বা git tag থেকে নিতে (setuptools-scm, hatch-vcs) — তাহলে git tag v0.2.0 দিলেই version ঠিক হয়।

Library-তে পরিসর, অ্যাপে pin

পর্ব ২৩-এর পরামর্শ ছিল: চালানোর উপযোগী অ্যাপে হুবহু pin. library-র ক্ষেত্রে উল্টো। ধরুন taka-format লিখল requests==2.31.0, আর ব্যবহারকারীর আরেকটি library চায় requests>=2.32. দুটি একসাথে ইনস্টল অসম্ভব — আপনার একটি অকারণ সংখ্যার জন্য অন্যের পুরো প্রজেক্ট আটকে গেল। library জানে না সে কার পাশে বসবে, তাই সে শুধু বলে “আমার কমপক্ষে কী লাগে” (requests>=2.31), আর জানা ভাঙন থাকলে সীমা (<3). হুবহু version ঠিক করার দায়িত্ব শেষ অ্যাপের, তার lock file-এ। ওপরের সীমা (upper bound) নিয়ে অভিজ্ঞদের মধ্যেও বিতর্ক আছে: অকারণ <3 ভবিষ্যতে সমাধানহীন জট বানাতে পারে, আবার না দিলে নতুন MAJOR ভাঙতে পারে — জানা কারণ ছাড়া কড়া সীমা দেবেন না।

এবার ব্যবহারকারীর দিক থেকে দেখুন — pip একটি পরিসর পেলে উপলব্ধ version-গুলো থেকে কীভাবে একটি বাছে (তালিকাটি বোঝানোর জন্য বানানো, requests-এর আসল release-তালিকা নয়):

1উপলব্ধ সব version-এর তালিকা2শর্ত ১: >=2.313শর্ত ২: <34pre-release বাদ (ডিফল্ট)5Requires-Python ও wheel মেলানো6সবচেয়ে বড়টি নেওয়া
প্রার্থী version
বাকি আছে
এইমাত্র বাদ
  1. তালিকা। pip index-এর পাতা থেকে package-এর সব version আর ফাইলের তালিকা আনে। এখানে সাতটি — পুরোনো স্থিতিশীল, নতুন স্থিতিশীল, একটি release candidate আর ভবিষ্যতের 3.x.
  2. >=2.31 — নিম্নসীমা। 2.28.2 বাদ। এটিই library-র লেখকের আসল বার্তা: “2.31-এর আগের কোনো সুবিধা আমার যথেষ্ট নয়”।
  3. <3 — ঊর্ধ্বসীমা। 3.0.0 বাদ, আর PEP 440-এর একটি সূক্ষ্ম নিয়মে 3.0.0b1-ও: <3 3-এর pre-release-কেও বাইরে রাখে, যদিও সংখ্যায় 3.0.0b1 < 3.0.0. কমা মানে “এবং” — দুটি শর্তই মানতে হবে।
  4. pre-release বাদ। 2.33.0rc1 পরিসরের ভেতরে হলেও pre-release, আর কেউ --pre চায়নি। তাই বাদ। কোনো স্থিতিশীল version না থাকলে তবেই pip pre-release বিবেচনা করে।
  5. পরিবেশ মেলানো। প্রতিটি প্রার্থীর Requires-Python আর wheel tag (অংশ ৫) আপনার Python ও OS-এর সাথে মেলানো হয়। না মিললে সেটিও বাদ যেত — এজন্যই অংশ ৩-এ requires-python সঠিক রাখা জরুরি।
  6. সবচেয়ে বড়টি। বাকিদের মধ্যে সবচেয়ে নতুন 2.32.3. বড় প্রজেক্টে resolver এই কাজ একসাথে সব dependency-র জন্য করে, আর কোনো বাছাই অন্যের শর্ত ভাঙলে পিছিয়ে এসে পরেরটি চেষ্টা করে।

৭ · Package data, editable install আর নতুন venv-এ পরীক্ষা

taka কমান্ড কোনো সংখ্যা না পেলে units.txt ফাইল থেকে লাখ-কোটির একটি ছোট সারণি দেখায়। .py নয় এমন ফাইল — JSON, CSV, টেমপ্লেট, ছবি — যা package-এর সাথে যায়, তাকে বলে package data. এটি পড়ার সঠিক উপায় importlib.resources:

# src/taka_format/cli.py (অংশ)
from importlib.resources import files

text = files("taka_format").joinpath("units.txt").read_text(encoding="utf-8")

কেন open("units.txt") নয়? কারণ সেটি বর্তমান ফোল্ডারে খোঁজে (পর্ব ২৬-এ দেখবেন), আর ব্যবহারকারী যেকোনো ফোল্ডার থেকে taka চালাতে পারেন। files("taka_format") package যেখানে ইনস্টল হয়েছে সেখান থেকেই খোঁজে — site-packages-এ হোক, editable হোক, এমনকি zip-এর ভেতরে হোক। আরেকটি কথা: ডেটা-ফাইল wheel-এ ঢুকবে কি না, তা backend-এর নিয়মে। hatchling package-ফোল্ডারের ফাইলগুলো ডিফল্টভাবে নেয় (আমাদের wheel-এ units.txt এসেছে), setuptools-এ package-data বা include-package-data সেটিং দেখতে হয়। নিয়ম মুখস্থ করার চেয়ে নিরাপদ অভ্যাস: প্রতিবার wheel খুলে দেখুন (অংশ ৫-এর কোড)।

Editable install — কাজ চলাকালীন

পর্ব ২৩-এ pip install -e . দেখেছেন। package-এর লেখক হিসেবে এটিই আপনার দৈনন্দিন অবস্থা: কোড বদলান, সঙ্গে সঙ্গে পরীক্ষা করুন, আবার ইনস্টল ছাড়াই। আমাদের venv-এ editable ইনস্টলের পর site-packages-এ কোডের কপি নয়, এসেছে _editable_impl_taka_format.pth নামে একটি ছোট নির্দেশক ফাইল আর .dist-info; আর taka_format.__file__ দেখিয়েছে সরাসরি src/taka_format/__init__.py. তবে মনে রাখুন: pyproject.toml বদলালে — নতুন dependency, নতুন কমান্ড, নতুন version — আবার pip install -e . দিতে হয়, কারণ সেগুলো metadata-য় থাকে, কোডে নয়।

প্রকাশের আগে শেষ পরীক্ষা — একদম নতুন venv

editable install আপনার ফোল্ডার দেখায়, ব্যবহারকারী পাবেন wheel. তাই প্রকাশের আগে wheel-টিকে একটি খালি venv-এ ইনস্টল করে ব্যবহারকারীর মতো চালান — প্রজেক্টের ফোল্ডারের বাইরে থেকে:

cd ..                                     # প্রজেক্টের বাইরে
python -m venv try-env
try-env\Scripts\Activate.ps1              # Mac/Linux: source try-env/bin/activate
python -m pip install taka-format/dist/taka_format-0.1.0-py3-none-any.whl
taka 1234567 10000000
taka
python -c "from importlib.metadata import version; print(version('taka-format'))"

প্রত্যাশিত আউটপুট (সংক্ষেপে) — আমাদের চালানো থেকে:

Successfully installed taka-format-0.1.0
৳ 12,34,567
৳ 1,00,00,000
taka-format 0.1.0
lakh = 1,00,000
crore = 1,00,00,000
0.1.0

দ্বিতীয় taka (কোনো সংখ্যা ছাড়া) version আর units.txt-এর লেখা দেখিয়েছে — অর্থাৎ package data সত্যিই wheel-এ গেছে আর importlib.resources সেটি খুঁজে পেয়েছে। এরপর এই venv-এ pytest ইনস্টল করে tests/ চালালে টেস্টগুলো চলবে ইনস্টল করা wheel-এর ওপর (পর্ব ৪২)। Windows-এর পুরোনো console-এ ৳ চিহ্ন ভাঙা দেখালে সেটি terminal-এর encoding-এর ব্যাপার, package-এর নয়।

৮ · প্রকাশ — TestPyPI, token আর Trusted Publishing

wheel পরীক্ষিত, এবার পৃথিবীর হাতে তুলে দেওয়ার পালা। প্রথমবারের প্রস্তুতি:

  1. দুটি অ্যাকাউন্ট: pypi.org আর TestPyPI (test.pypi.org) — দুটি সম্পূর্ণ আলাদা সাইট, আলাদা অ্যাকাউন্ট। TestPyPI অনুশীলনের মাঠ: এখানে ভুল করলে কারও ক্ষতি নেই, আর মাঝে মাঝে সেখানকার ডেটা মুছেও ফেলা হয়।
  2. 2FA চালু: ২০২৪ সালের শুরু থেকে PyPI-তে আপলোড করতে সব অ্যাকাউন্টে 2FA বাধ্যতামূলক — authenticator অ্যাপ বা security key. recovery code নিরাপদে রেখে দিন।
  3. নাম যাচাই: দুটি সাইটেই নামটি খালি কি না দেখুন (অংশ ২-এর normalization মনে রেখে)।

প্রথম পথ — API token আর twine

পাসওয়ার্ড দিয়ে আপলোড PyPI বহু আগেই বন্ধ করেছে। এর বদলে API token: অ্যাকাউন্টের সেটিং থেকে বানানো একটি লম্বা গোপন লেখা, যা pypi- দিয়ে শুরু হয়। প্রথম আপলোডের জন্য পুরো অ্যাকাউন্টের token লাগে; প্রজেক্ট তৈরি হয়ে গেলে শুধু ওই প্রজেক্টে সীমাবদ্ধ (project-scoped) token বানিয়ে পুরোনোটি মুছে দিন — চুরি হলেও ক্ষতি এক প্রজেক্টে সীমিত থাকে।

# ১. আগে TestPyPI-তে (username চাইলে লিখুন __token__, password-এ token)
twine upload --repository testpypi dist/*

# ২. নতুন venv-এ TestPyPI থেকে ইনস্টল করে পরীক্ষা
python -m pip install --index-url https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple/ taka-format

# ৩. সব ঠিক থাকলে আসল PyPI-তে
twine upload dist/*

# uv ব্যবহার করলে একই কাজ:  uv build  তারপর  uv publish

দ্বিতীয় ধাপের --extra-index-url এখানে দরকার, কারণ আপনার package-এর dependency-গুলো TestPyPI-তে নাও থাকতে পারে। তবে এটি শুধু পরীক্ষার জন্য — দুটি index একসাথে দেওয়া কেন কোম্পানির কাজে বিপজ্জনক, তা অংশ ৯-এ দেখবেন। token কোথায় রাখবেন? প্রতিবার হাতে পেস্ট, বা OS-এর keyring-এ (twine এটি সমর্থন করে), বা নিজের হোম ফোল্ডারের .pypirc ফাইলে — কিন্তু কখনো প্রজেক্টের ফোল্ডারে বা git-এ নয়। GitHub-এ পাবলিক কোডে ফাঁস হওয়া PyPI token GitHub-এর secret scanning-এর মাধ্যমে PyPI-কে জানানো হয়, আর PyPI সেটি বাতিল করে দেয় — তবু সেই ভরসায় থাকবেন না।

আরও ভালো পথ — Trusted Publishing

token-এর মূল সমস্যা: এটি দীর্ঘস্থায়ী একটি চাবি। CI-তে (যেমন GitHub Actions) স্বয়ংক্রিয় প্রকাশের জন্য সেটি repository-র secret-এ রাখতে হয়, আর যেকোনো জায়গায় জমা থাকা চাবি একদিন চুরি হতে পারে। ২০২৩ সালে PyPI-তে আসা trusted publishing-এ জমা রাখার মতো কোনো চাবিই থাকে না। আপনি PyPI-কে আগে থেকে বলে রাখেন: “এই GitHub repository-র এই workflow ফাইল থেকে আসা অনুরোধ আমার প্রজেক্টে আপলোড করতে পারবে”। তারপর প্রতিবার প্রকাশের সময় GitHub একটি স্বল্পমেয়াদি, স্বাক্ষর করা পরিচয়পত্র দেয় — OIDC token — আর PyPI সেটি যাচাই করে কয়েক মিনিটের জন্য একটি আপলোড-token দেয়। ধাপে ধাপে:

  1. আগে থেকে চুক্তিPyPI → Publishingrepo + workflow
  2. Releasev0.1.1workflow শুরু
  3. OIDC tokenid-token: writeGitHub স্বাক্ষর করে
  4. PyPI যাচাইস্বাক্ষর + দাবিমিললে স্বল্পমেয়াদি token
  5. আপলোডdist/*token মেয়াদোত্তীর্ণ
pypi.org — প্রজেক্টের Publishing সেটিংএকবারই করতে হয়
Owner:        nusrat-dev
Repository:   taka-format
Workflow:     publish.yml
Environment:  pypi          (ঐচ্ছিক, কিন্তু ভালো)
নতুন প্রজেক্টে "pending publisher" হিসেবে আগেই যোগ করা যায়
.github/workflows/publish.ymlপ্রথম অংশ
on:
  release:
    types: [published]
jobs:
  pypi:
    runs-on: ubuntu-latest
    environment: pypi
publish.yml — অনুমতিকোনো secret নেই
    permissions:
      id-token: write    ← OIDC token চাওয়ার অনুমতি
    steps:
      - uses: actions/checkout@v4
      - run: pipx run build
PyPI যা মিলিয়ে দেখেtoken-এর ভেতরের দাবি
স্বাক্ষর GitHub-এর?        ✓
repository = nusrat-dev/taka-format   ✓
workflow   = publish.yml              ✓
environment = pypi                    ✓
→ শুধু taka-format-এর জন্য স্বল্পমেয়াদি API token
publish.yml — শেষ ধাপPyPA-র অফিসিয়াল action
      - uses: pypa/gh-action-pypi-publish@release/v1
token বিনিময় + আপলোড এই action-ই করে;
কয়েক মিনিট পরে token নিজেই অকেজো
  1. আগে থেকে বিশ্বাসের চুক্তি। PyPI-তে প্রজেক্টের সেটিং-এ একটি “trusted publisher” যোগ করেন: GitHub-এর মালিক, repository, workflow ফাইলের নাম, আর ঐচ্ছিকভাবে environment-এর নাম। প্রজেক্ট এখনো না থাকলে “pending publisher” হিসেবে আগেই যোগ করা যায় — প্রথম আপলোডেই প্রজেক্ট তৈরি হবে। কোথাও কোনো token কপি হলো না।
  2. Release দিলে workflow শুরু। নুসরাত GitHub-এ v0.1.1 release প্রকাশ করলেন, আর publish.yml চলতে শুরু করল। environment: pypi দিলে GitHub-এ ওই environment-এ বাড়তি সুরক্ষা বসানো যায় — যেমন প্রকাশের আগে কারও হাতে অনুমোদন।
  3. OIDC token চাওয়া। permissions: id-token: write থাকলেই job GitHub-এর কাছে একটি OIDC token চাইতে পারে। এটি GitHub-এর স্বাক্ষর করা একটি ছোট পরিচয়পত্র, যার ভেতরে লেখা — কোন repository, কোন workflow, কোন branch বা tag, কোন environment. মেয়াদ খুব কম। repository-র secret-এ কিছুই রাখা হয়নি।
  4. PyPI যাচাই করে। action টি token PyPI-কে পাঠায়। PyPI GitHub-এর প্রকাশ্য চাবি দিয়ে স্বাক্ষর যাচাই করে, তারপর ভেতরের দাবিগুলো ধাপ ১-এর চুক্তির সাথে মেলায়। সব মিললে দেয় শুধু এই প্রজেক্টের জন্য একটি স্বল্পমেয়াদি API token (কয়েক মিনিটের)। অন্য কোনো repository বা ফর্ক করা কপি থেকে চাইলে দাবি মিলবে না।
  5. আপলোড, তারপর চাবি নেই। সেই token দিয়ে dist/-এর ফাইল আপলোড হয়, আর কিছুক্ষণ পর token নিজেই অকেজো। কেউ log থেকে চুরি করলেও লাভ নেই। PyPA-র এই action এখন প্রকাশিত ফাইলের জন্য ডিজিটাল attestation-ও (PEP 740) তৈরি করে — কোন workflow ফাইলটি বানিয়েছে তার স্বাক্ষর করা প্রমাণ। ভালো অভ্যাস: build একটি job-এ, আর id-token অনুমতিসহ আপলোড আলাদা একটি ছোট job-এ।

GitHub ছাড়াও GitLab CI/CD, Google Cloud আর ActiveState থেকে trusted publishing সমর্থিত। আর uv publish-ও GitHub Actions-এ চললে trusted publishing ব্যবহার করতে পারে।

৯ · দল ও কোম্পানির জন্য — private index, project manager আর নিরাপত্তা

সব package পৃথিবীর জন্য নয়। ধরুন একটি বাংলাদেশি ফিনটেক কোম্পানির তিনটি দল একই bKash/Nagad পেমেন্ট-ক্লায়েন্ট কোড ব্যবহার করে, যার ভেতরে কোম্পানির নিজস্ব নিয়ম আছে। এটিকেও একই উপায়ে package বানানো হয় — শুধু প্রকাশ হয় কোম্পানির ভেতরের ভাণ্ডারে:

  • ছোট দলের জন্য: সরাসরি git থেকে, নির্দিষ্ট tag দিয়ে — pip install "git+https://github.com/acme/paykit@v1.4.0". index লাগে না, তবে প্রতি ইনস্টলে build হয়, আর tag বদলানো যায় বলে পুরোপুরি স্থির নয়।
  • প্রতিষ্ঠানের জন্য private package index: PyPI-র মতোই একটি সার্ভার, শুধু ভেতরের লোকদের জন্য — নিজে চালানো যায় (devpi, Sonatype Nexus, JFrog Artifactory), বা ক্লাউডের সেবা (AWS CodeArtifact, Azure Artifacts, Google Artifact Registry, GitLab-এর package registry). প্রকাশ হয় সেই একই twine upload --repository-url ... বা uv publish --publish-url ... দিয়ে।

--extra-index-url-এর ফাঁদ: কোম্পানির package-এর জন্য pip install --extra-index-url https://pkgs.acme.local/simple/ paykit দিলে pip দুটি index-কেই সমান ধরে, আর যেখানে বড় version পায়, সেটিই নেয়। কেউ পাবলিক PyPI-তে paykit নামে বড় version নম্বরের ক্ষতিকর package রাখলে সেটিই আসবে — পর্ব ২৩-এর dependency confusion. প্রতিরোধ: একটিমাত্র --index-url দিন, যা private index — আর সেই index-ই PyPI-র package proxy করে আনবে; অথবা uv-এর মতো টুল ব্যবহার করুন, যার ডিফল্ট নিয়মে প্রথম যে index-এ নাম পাওয়া যায়, শুধু সেখান থেকেই নেয়; আর ভেতরের নামগুলো দরকার হলে পাবলিক PyPI-তে আগেই নিবন্ধন করে রাখুন। ভেতরের package-এ Private :: Do Not Upload classifier ভুল আপলোড আটকায় (অংশ ৩)।

uv, Poetry, PDM — পুরো প্রজেক্টের ব্যবস্থাপক

এই পর্বে আমরা ইচ্ছা করেই আলাদা আলাদা টুল (build, twine, pip) ব্যবহার করেছি, যাতে প্রতিটি ধাপ চোখে পড়ে। বাস্তবে অনেক দল একটি project manager ব্যবহার করে, যা venv, dependency, lock file, build আর publish এক জায়গায় সামলায়। মনে রাখার বিষয়: এরা সবাই শেষে একই মানক wheel আর sdist বানায়, আর একই PyPI-তে পাঠায় — তাই ব্যবহারকারী জানতেও পারেন না আপনি কোনটি ব্যবহার করেছেন।

⚡ uv

uv init --package src layout-সহ প্রজেক্ট বানায়, uv build আর uv publish বাকি কাজ করে। খুব দ্রুত, Astral কোম্পানির open source টুল।

uv · uv.lock

📜 Poetry

২০১৮ থেকে জনপ্রিয়, নিজস্ব backend poetry-core. Poetry 2.0 (২০২৫) থেকে মানক [project] অংশ সমর্থন করে। poetry build, poetry publish.

Poetry · poetry.lock

🧩 PDM

মানক-ঘনিষ্ঠ project manager, pdm-backend-এর সাথে। pdm build, pdm publish.

pdm.lock

প্রকাশকের নিরাপত্তা-দায়িত্ব

পর্ব ২৩-এ আপনি ছিলেন ব্যবহারকারী — অচেনা package থেকে নিজেকে বাঁচানো। এখন আপনি প্রকাশক, আর অন্যরা আপনাকে বিশ্বাস করে pip install দেবেন। Supply-chain attack-এর সবচেয়ে মূল্যবান লক্ষ্য হলো জনপ্রিয় package-এর প্রকাশকের অ্যাকাউন্ট, কারণ তখন আসল নামের package-ই ক্ষতিকর হয়ে যায়।

  • অ্যাকাউন্ট: 2FA (বাধ্যতামূলক), ভালো হলে hardware security key; অ্যাকাউন্টের ইমেইলের ডোমেইন সচল রাখুন।
  • token: সম্ভব হলে trusted publishing; না হলে project-scoped token, git-এ কখনো নয়, .gitignore-এ .pypirc আর .env.
  • যা প্রকাশ করছেন তা যাচাই: wheel আর sdist খুলে দেখুন — ভুল করে .env, পাসওয়ার্ডের ফাইল বা কোম্পানির ডেটা ঢুকে যায়নি তো? sdist-এ ডিফল্টভাবে অনেক ফাইল যায়।
  • Typosquatting-এর উল্টো দিক: আপনার package জনপ্রিয় হলে কেউ কাছাকাছি বানানে (taka-fromat) নকল ছাড়তে পারে। README-তে সঠিক ইনস্টল-কমান্ড স্পষ্ট লিখুন, সন্দেহজনক নকল দেখলে PyPI-তে রিপোর্ট করুন।
  • নিজের dependency কম রাখুন: আপনার প্রতিটি dependency আপনার ব্যবহারকারীরও dependency. পাঁচ লাইনের কাজের জন্য নতুন package নয়।
পরীক্ষা ও ইন্টারভিউতে
  • wheel আর sdist-এর পার্থক্য? wheel আগে থেকে build করা zip, ইনস্টল মানে কপি; sdist সোর্স-আর্কাইভ, ইনস্টলের সময় build লাগে।
  • build frontend আর backend কী? frontend (pip, build, uv) build শুরু করে ও পরিবেশ বানায়; backend (setuptools, hatchling) [build-system] অনুযায়ী আসল ফাইল বানায় (PEP 517).
  • src layout কেন? ইনস্টল না করে package import হওয়া আটকায়, তাই টেস্ট চলে ইনস্টল করা কপিতে।
  • library-তে == দিয়ে pin করা খারাপ কেন? ব্যবহারকারীর অন্য package-এর সাথে অমীমাংসেয় version-জট তৈরি করে; library-তে পরিসর, অ্যাপে lock.
  • Trusted publishing কী? CI-র স্বল্পমেয়াদি OIDC পরিচয় দিয়ে PyPI-তে প্রকাশ, কোনো দীর্ঘস্থায়ী token জমা না রেখে।

১০ · AI-এর যুগে — AI-এর লেখা packaging কনফিগ যাচাই

“আমার কোডকে PyPI-তে দেওয়ার মতো package বানিয়ে দাও” — AI কয়েক সেকেন্ডে পুরো কনফিগ লিখে দেয়। সমস্যা হলো, packaging-এর নিয়ম গত কয়েক বছরে দ্রুত বদলেছে, আর AI-এর শেখার লেখার বড় অংশ পুরোনো যুগের। সাধারণ ভুলগুলো:

AI যা লেখেসমস্যাসঠিক পথ
শুধু setup.py + setup.cfg, pyproject.toml নেইপুরোনো যুগের পদ্ধতি; build-এর আগে কী লাগবে কেউ জানে না[build-system] + [project]-সহ pyproject.toml
python setup.py sdist upload বা setup.py installবহু আগে বাতিলের পথে; পাসওয়ার্ড-আপলোড বন্ধpython -m build, twine upload / uv publish
license = {text = "MIT"} বা License :: classifierPEP 639-এর আগের রূপ; নতুন টুল সতর্ক করে বা error দেয়license = "MIT" + license-files
library-র dependency-তে requests==2.31.0ব্যবহারকারীর প্রজেক্টে version-জটrequests>=2.31 (দরকারে জানা ঊর্ধ্বসীমা)
CI-ফাইলে TWINE_PASSWORD: pypi-AgE... সরাসরি লেখাtoken git-এর ইতিহাসে চিরকাল ফাঁসtrusted publishing, অন্তত repository secret
version দুই জায়গায়, requires-python নেইঅমিল version, পুরোনো Python-এ অদ্ভুত errorএক জায়গায় version, importlib.metadata; requires-python দিন
ভেতরের package-এর জন্য --extra-index-urldependency confusionএকক private index

AI-কে packaging-এর কাজ দেওয়ার সময় আধুনিক নিয়মগুলো শুরুতেই বেঁধে দিন, আর সিদ্ধান্তগুলোর কারণ জানতে চান:

আমার একটি বিশুদ্ধ Python প্রজেক্ট আছে: src/taka_format/ ফোল্ডারে একটি package, যা টাকার অঙ্ক লাখ-কোটি নিয়মে সাজায়, আর একটি cli.py-তে main() ফাংশন। আমি এটি PyPI-তে প্রকাশ করতে চাই। নিয়মগুলো মেনে কাজ করো: ১. শুধু pyproject.toml ব্যবহার করবে (setup.py নয়), src layout রাখবে, backend হিসেবে hatchling, আর কোন ন্যূনতম version লাগবে তার কারণ বলবে। ২. license লিখবে PEP 639 অনুযায়ী SPDX expression হিসেবে (license = "MIT"), License :: classifier দেবে না। ৩. এটি একটি library — dependency থাকলে == দিয়ে pin করবে না, পরিসর দেবে, আর requires-python দেবে। ৪. version শুধু pyproject.toml-এ থাকবে; কোডে importlib.metadata দিয়ে পড়বে। ৫. "taka" নামে console script যোগ করো। ৬. GitHub Actions workflow লিখবে trusted publishing দিয়ে (id-token: write, pypa/gh-action-pypi-publish) — কোনো token বা পাসওয়ার্ড ফাইলে লিখবে না। ৭. প্রকাশের আগে আমি নিজে কী কী যাচাই করব (wheel খুলে দেখা, নতুন venv-এ পরীক্ষা, TestPyPI), তার তালিকা দাও। কোনো তথ্য নিয়ে নিশ্চিত না হলে স্পষ্ট বলো।

AI-এর দেওয়া packaging কনফিগ প্রকাশের আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো packaging-এর “কীভাবে” থেকে নকশার “কেন”-তে নিয়ে যাবে।

প্র ০১distutils ২০০০ সাল থেকে Python-এর ভেতরেই ছিল — তাহলে সেটিকে standard library থেকে সরাতে প্রায় দুই দশক লাগল কেন, আর এতে কী শেখার আছে?

distutils (Python 1.6, ২০০০): Greg Ward-এর নেতৃত্বে তৈরি এই module প্রথমবার Python-এ “package বানানো ও ইনস্টল করার” একটি মানক পথ দেয় — setup.py লিখে python setup.py install. standard library-র অংশ হওয়ায় সবার কাছে ছিল, এটিই বড় সুবিধা।

সমস্যা — standard library-র গতি: standard library-র কোড বদলায় শুধু নতুন Python release-এর সাথে, আর পুরোনো Python বহু বছর চালু থাকে। packaging-এর চাহিদা (dependency, নতুন ফরম্যাট, নতুন প্ল্যাটফর্ম) বদলাচ্ছিল অনেক দ্রুত। তাই উন্নতি হতে থাকে বাইরে: setuptools (২০০৪) distutils-এর ওপর dependency, entry point, egg যোগ করে; ২০০৮ সালে এর ফর্ক distribute আসে, যা ২০১৩ সালে আবার setuptools-এ মিশে যায়। বাস্তবে সবাই setuptools ব্যবহার করত, অথচ distutils standard library-তে বসে থাকত — পুরোনো, অর্ধেক-রক্ষণাবেক্ষণ করা, আর setuptools-কে ভেতরে ভেতরে তার ওপর patch করতে হতো।

মানক দিয়ে মুক্তি: সরানো তখনই সম্ভব হলো, যখন “কীভাবে build হবে” প্রশ্নটি কোনো একটি টুল থেকে আলাদা করে একটি চুক্তিতে পরিণত হলো। PEP 518 (২০১৬) [build-system] আনে, PEP 517 (২০১৭) frontend ও backend-এর মানক ফাংশন ঠিক করে; pip 10 (২০১৮) pyproject.toml পড়া শুরু করে, আর pip 19 (২০১৯) থেকে PEP 517 build. PEP 621 (২০২০) metadata-কে টুলনিরপেক্ষ [project] অংশে আনে। এবার আর distutils-এর দরকার নেই — যেকোনো backend চলবে।

বিদায়: PEP 632 (২০২০) distutils-কে Python 3.10-এ deprecated ঘোষণা করে, আর Python 3.12 (২০২৩) থেকে এটি সম্পূর্ণ সরানো। যাঁদের এখনো লাগে, setuptools নিজের ভেতরে distutils-এর একটি কপি রেখে দেয়। সরানোর আগে কয়েক বছরের সতর্কবার্তা ছিল, যাতে পুরোনো প্রজেক্ট সময় পায়।

এখানে একটি সাধারণ নকশা-শিক্ষা আছে: ভাষার ভেতরে যা ঢোকে, তা স্থির হয় কিন্তু ধীর হয়ে যায় — Python জগতে প্রবাদ আছে, “standard library হলো যেখানে module মরতে যায়”। দ্রুত বদলানো ক্ষেত্রে ভাষার কাজ টুল দেওয়া নয়, মানক ইন্টারফেস দেওয়া; টুলগুলো বাইরে প্রতিযোগিতা করে উন্নত হয়।

মূল উপলব্ধি: distutils থেকে PEP 517/621-এর যাত্রা হলো “একটি সরকারি টুল” থেকে “একটি সরকারি চুক্তি”-তে যাওয়া। চুক্তি স্থির থাকলে টুল বদলানো যায় — এজন্যই আজ hatchling থেকে uv_build-এ যেতে দুটি লাইন বদলালেই চলে।

প্র ০২egg তো আগে থেকেই ছিল — তাহলে wheel (PEP 427) নামে নতুন ফরম্যাট আনার দরকার পড়ল কেন? দুটিই তো zip!

setuptools-এর সাথে আসা egg (মোটামুটি ২০০৪) আর পরে আসা wheel দুটিই zip-ভিত্তিক — পার্থক্যটা উদ্দেশ্যে। egg একসাথে দুটি কাজ করতে চাইত: বিতরণের ফরম্যাট, আবার সরাসরি import করার ফরম্যাট। একটি .egg ফাইল sys.path-এ বসিয়ে দিলেই তার ভেতর থেকে import চলত। শুনতে চতুর, কিন্তু এর দাম ছিল: এক package-এর একাধিক version পাশাপাশি রাখতে .pth ফাইল আর runtime-এ sys.path বদলানোর জটিল কৌশল, zip-এর ভেতর থেকে ডেটা-ফাইল পড়ার ঝামেলা, আর ধীর import.

আরও বড় সমস্যা ছিল মানকের অভাব। egg-এর metadata (EGG-INFO) কোনো PEP-এ ঠিকভাবে লেখা ছিল না — কার্যত “setuptools যা করে, তা-ই নিয়ম”। কোন egg কোন Python বা কোন OS-এর জন্য, তা প্রকাশের কোনো সম্পূর্ণ ও নির্ভরযোগ্য ব্যবস্থা ছিল না। আর ইনস্টলের সময় কোন ফাইল কোথায় গেল, তার মানক রসিদও ছিল না, তাই uninstall অনির্ভরযোগ্য।

wheel (PEP 427, ২০১২ সালে প্রস্তাবিত, ২০১৩-তে গৃহীত, Daniel Holth): নকশার মূল সিদ্ধান্ত — wheel শুধু ইনস্টলের ফরম্যাট, import-এর নয়। ইনস্টলার এটি খুলে site-packages-এ সাধারণ ফাইল হিসেবে বসায়, তারপর Python সাধারণ ফোল্ডার থেকেই import করে। সাথে আসে স্পষ্ট মানক: ফাইলের নামে compatibility tag (PEP 425 — python, abi, platform), .dist-info-তে মানক METADATA, আর RECORD-এ প্রতিটি ফাইলের hash — যা দিয়ে uninstall আর অখণ্ডতা যাচাই দুটোই হয়। ইনস্টলের সময় কোনো কোড চলে না, তাই দ্রুত ও নিরাপদ।

পরিণতি বিশাল: NumPy-র মতো C-নির্ভর package Windows আর Mac-এ compiler ছাড়াই ইনস্টল হতে শুরু করে, আর manylinux মানক (PEP 513, ২০১৬) Linux-এও তা সম্ভব করে। Python-এর বৈজ্ঞানিক জগতের বিস্তারের পেছনে এর অবদান কম নয়। egg ধীরে ধীরে বিদায় নেয়; ২০২৩ সালে PyPI নতুন egg আপলোড নেওয়া বন্ধ করে।

মূল উপলব্ধি: একটি ফরম্যাট দুটি দায়িত্ব নিলে দুটিই খারাপ হয়। wheel জিতেছে কম কাজ করে — শুধু বিতরণ — আর সেই কাজটি নিখুঁত মানকে বেঁধে।

প্র ০৩২০১৬ সালে npm-এর ১১ লাইনের left-pad package সরিয়ে নেওয়ায় হাজারো প্রজেক্ট ভেঙে পড়ে। এমনটা কি Python-এ হতে পারে, আর একজন package-লেখক হিসেবে এ থেকে কী শিখবেন?

ঘটনা (মার্চ ২০১৬): একজন JavaScript ডেভেলপার, Azer Koçulu, একটি package-এর নাম নিয়ে কোম্পানির সাথে বিরোধের পর প্রতিবাদে npm থেকে নিজের প্রায় আড়াইশোর মতো package সরিয়ে নেন। তার একটি ছিল left-pad — একটি string-এর বাঁয়ে ফাঁকা ঘর বসানোর ১১ লাইনের ফাংশন। দেখা গেল, Babel-এর মতো বিশাল টুলসহ অসংখ্য প্রজেক্ট transitive dependency হিসেবে এর ওপর নির্ভর করত। কয়েক ঘণ্টার জন্য পৃথিবীজুড়ে build ভাঙতে থাকে। npm অস্বাভাবিক পদক্ষেপ নিয়ে package-টি ফিরিয়ে আনে, আর পরে নিয়ম বদলায়: প্রকাশের কিছু সময় পর, অন্যরা নির্ভর করলে, package সরানো কঠিন করা হয়।

Python-এ কী অবস্থা? PyPI-তে মালিক এখনো নিজের প্রজেক্ট বা release মুছতে পারেন — তাই একই ধরনের ঝুঁকি তাত্ত্বিকভাবে আছে। তবে দুটি সুরক্ষা আছে: মুছে ফেলা ফাইলের নাম আর কখনো ব্যবহার করা যায় না, ফলে কেউ মুছে একই নাম-version-এ অন্য কোড বসাতে পারে না; আর PEP 592 (২০১৯) এনেছে “yank” — মোছার বদলে নরম পথ, যাতে নতুন ইনস্টলে খারাপ version না আসে কিন্তু যাঁরা হুবহু pin করেছেন তাঁদের build না ভাঙে। Python-এর বড় standard library-ও সাহায্য করে — ছোটখাটো কাজের জন্য (যেমন str.rjust, যা left-pad-এর কাজই করে) বাইরের package লাগে না।

গভীর শিক্ষা — নির্ভরতা মানে বিশ্বাস: প্রতিটি dependency মানে কারও ভবিষ্যৎ সিদ্ধান্তের ওপর নির্ভর করা — তিনি package রাখবেন কি না, অ্যাকাউন্ট সুরক্ষিত রাখবেন কি না, ভাঙা version দেবেন কি না। প্রতিরক্ষার স্তর: lock file আর hash (যা আছে তা-ই আসবে), কোম্পানির private index-এ PyPI-র proxy বা mirror (উৎস হারালেও কপি থাকে), আর খুব ছোট dependency হলে নিজে লিখে ফেলা বা কোড প্রজেক্টের ভেতরে এনে রাখা (vendoring).

লেখক হিসেবে: আপনার package-এ যত কম dependency, আপনার ব্যবহারকারীর ঝুঁকি তত কম। আর প্রকাশিত কিছু সরাতে চাইলে মোছার বদলে yank করুন, নতুন version-এ deprecation-বার্তা দিন — অন্যের build ভাঙবেন না।

মূল উপলব্ধি: open source ইকোসিস্টেম একটি সামাজিক চুক্তি। প্রকাশ করা মানে অন্যকে আপনার ওপর নির্ভর করার আমন্ত্রণ দেওয়া — সেই দায়িত্বের সাথে আচরণ করতে হয়।

প্র ০৪PyPI-তে ক্ষতিকর package আর typosquatting-এর ঢেউ বারবার আসে। ভাণ্ডারটি প্রকাশকের দিক থেকে কোন কোন প্রতিরক্ষা গড়ে তুলেছে, আর কেন “সব package আগে পরীক্ষা করো” সমাধান নয়?

পর্ব ২৩-এ ব্যবহারকারীর দিক থেকে কয়েকটি ঘটনা দেখেছেন। প্রকাশকের দিক থেকে ছবিটা এমন: আক্রমণের দুই বড় ধরন — নতুন নামে নকল (typosquatting, পরিচিত নামের কাছাকাছি বানান) আর আসল নামের দখল (প্রকাশকের অ্যাকাউন্ট বা token চুরি)। প্রথমটি অনেক মানুষকে অল্প অল্প ধরে, দ্বিতীয়টি একবারে বিপুল ক্ষতি করে, কারণ তখন সবাই বিশ্বাস করা নামটিই ইনস্টল করেন।

কেন আগে পরীক্ষা নয়? PyPI-তে প্রতিদিন বিপুল সংখ্যক নতুন release আসে, আর ভাণ্ডারটি মূলত স্বেচ্ছাসেবক ও অল্প কয়েকজন কর্মী দিয়ে চলে। প্রতিটি release হাতে পরীক্ষা অসম্ভব, আর স্বয়ংক্রিয় স্ক্যানার চতুর আক্রমণকারীকে সবসময় ধরতে পারে না — উল্টে অনুমোদনের অপেক্ষা পুরো ইকোসিস্টেমকে ধীর করত। তাই PyPI-র কৌশল হলো আক্রমণের খরচ বাড়ানো আর ধরা পড়লে দ্রুত সরানো।

প্রতিরক্ষার স্তরগুলো:

  • পরিচয় শক্ত করা: API token (২০১৯) — পাসওয়ার্ডের বদলে সীমিত-অধিকারের চাবি; ২০২৪-এর শুরু থেকে সব আপলোডকারীর জন্য 2FA বাধ্যতামূলক।
  • চাবি না রাখা: trusted publishing (২০২৩) — চুরির মতো স্থায়ী token-ই নেই; সাথে attestation (PEP 740, ২০২৪) — কোন repository ও workflow ফাইলটি বানিয়েছে তার স্বাক্ষরিত প্রমাণ।
  • ফাঁস ধরা: GitHub-এর secret scanning-এর সাথে যুক্ত থেকে পাবলিক কোডে পাওয়া PyPI token স্বয়ংক্রিয় বাতিল।
  • দ্রুত প্রতিক্রিয়া: নিরাপত্তা গবেষক ও কোম্পানিগুলোর রিপোর্ট নেওয়ার ব্যবস্থা, ক্ষতিকর প্রজেক্ট দ্রুত সরানো বা আটকে রাখা; বড় আক্রমণের ঢেউয়ের সময় কয়েকবার সাময়িকভাবে নতুন অ্যাকাউন্ট বা প্রজেক্ট তৈরি বন্ধ রাখা হয়েছে।
  • নামের নিয়ম: normalization-এর পর একই হয়ে যাওয়া নাম নেওয়া যায় না, তাই বড়-ছোট হাতের অক্ষর বা -/_-এর খেলা দিয়ে হুবহু নকল নাম বানানো যায় না।

তবু শেষ রক্ষাকবচ মানুষ: প্রকাশক অ্যাকাউন্ট সুরক্ষিত রাখবেন, ব্যবহারকারী নাম যাচাই করবেন আর pin ও hash ব্যবহার করবেন।

মূল উপলব্ধি: খোলা ভাণ্ডারে নিখুঁত পাহারা অসম্ভব; কার্যকর নিরাপত্তা আসে স্তরে স্তরে — প্রতিটি স্তর আক্রমণকে একটু কঠিন, একটু ব্যয়বহুল আর একটু দ্রুত ধরা পড়ার মতো করে।

প্র ০৫“আমার কম্পিউটারে চলে” থেকে হুবহু পুনরুৎপাদনযোগ্য ইনস্টল — Python-এ lock file-এর ইতিহাস কেমন, আর PEP 751-এর pylock.toml কী সমাধান করতে চায়?

pyproject.toml বলে “কী লাগে”, কিন্তু একই পরিসর থেকে আজ আর ছয় মাস পরে ভিন্ন version আসে। চালানোর উপযোগী অ্যাপের দরকার হুবহু ছবি — প্রতিটি package, transitive-সহ, নির্দিষ্ট version, আর আদর্শভাবে প্রতিটি ফাইলের hash. এটিই lock file.

প্রথম যুগ — pip freeze: হুবহু version দেয়, কিন্তু hash নেই, আর এটি শুধু এই কম্পিউটারের ছবি — Windows-এ freeze করা তালিকায় Linux-এ লাগে এমন dependency না-ও থাকতে পারে। pip-tools-এর pip-compile হাতে লেখা তালিকা থেকে hash-সহ ফাইল বানায়, আর pip install --require-hashes hash না মিললে ইনস্টল থামায়।

টুল-ভিত্তিক lock: Pipenv-এর Pipfile.lock (২০১৭), Poetry-র poetry.lock (২০১৮), PDM-এর pdm.lock, আর uv-এর uv.lock (২০২৪) — যা “universal”, অর্থাৎ এক ফাইলে সব OS আর Python version-এর সমাধান রাখে। প্রতিটি ভালো, কিন্তু প্রতিটির ফরম্যাট আলাদা, আর অন্য টুল তা পড়তে পারে না। ক্লাউড প্ল্যাটফর্ম বা নিরাপত্তা-স্ক্যানারকে তাই প্রতিটি ফরম্যাট আলাদা করে বুঝতে হয়।

মানকের চেষ্টা: একটি সাধারণ lock-ফরম্যাটের প্রথম প্রস্তাব PEP 665 ২০২২ সালে প্রত্যাখ্যাত হয় — অন্যতম কারণ, এটি sdist সমর্থন করত না, যা অনেকের কাছে অগ্রহণযোগ্য ছিল। Brett Cannon-এর PEP 751 বহু আলোচনার পর ২০২৫ সালে গৃহীত হয়: pylock.toml, একটি TOML ফাইল, যাতে প্রতিটি package-এর version, উৎস আর hash থাকে, এবং যা ইনস্টল করতে resolver চালাতে হয় না — শুধু পড়ে বসিয়ে দেওয়া। টুলগুলো ধীরে ধীরে সমর্থন যোগ করছে: pip 25.1-এ পরীক্ষামূলক pip lock কমান্ড এসেছে, uv এই ফরম্যাটে export করতে পারে। তবে এখনো সব টুল নিজের ফরম্যাটই প্রধান হিসেবে রাখে; ভবিষ্যতে কতটা একীভূত হবে, তা সময় বলবে।

library-লেখকের জন্য একটি সূক্ষ্মতা: lock file আপনার নিজের CI আর ডেভেলপমেন্টকে পুনরুৎপাদনযোগ্য রাখে, কিন্তু wheel-এর সাথে ব্যবহারকারীর কাছে যায় না — ব্যবহারকারী পান শুধু METADATA-র পরিসর। তাই library-তে lock রাখা ঠিক আছে, কিন্তু সেটিকে dependency-নীতি ভাববেন না।

মূল উপলব্ধি: “কী চাই” (পরিসর) আর “ঠিক কী পেলাম” (lock) দুটি আলাদা প্রশ্ন। Python-এর ইকোসিস্টেম দুটিকে আলাদা ফাইলে মানক করতে প্রায় এক দশক লাগিয়েছে — প্রথমটি PEP 621-এ, দ্বিতীয়টি PEP 751-এ।

অনুশীলন

প্রথম তিনটি ভাবনার কাজ, শেষটি কোড লেখার। নিজের কম্পিউটার থাকলে শেষটি সত্যিই build করে দেখুন।

  1. wheel-এর নাম পড়ুন: নিচের তিনটি ফাইল কোথায় কোথায় ইনস্টল হবে? (ক) taka_format-0.1.0-py3-none-any.whl (খ) fastcalc-1.2.0-cp313-cp313-win_amd64.whl (গ) fastcalc-1.2.0-cp310-abi3-manylinux_2_28_x86_64.whl. Python 3.14, 64-বিট Windows-এ fastcalc ইনস্টল করতে চাইলে pip কী করবে?
    • (ক) যেকোনো Python 3, যেকোনো OS — বিশুদ্ধ Python.
    • (খ) শুধু CPython 3.13, 64-বিট Windows.
    • (গ) CPython 3.10 বা নতুন যেকোনো version (stable ABI), কিন্তু শুধু glibc 2.28 বা নতুন x86-64 Linux-এ।

    Python 3.14 + Windows-এ (খ) মেলে না (ভুল Python), (গ) মেলে না (ভুল OS). তাই pip উপযুক্ত wheel না পেয়ে sdist খুঁজবে; পেলে আপনার কম্পিউটারে build করার চেষ্টা করবে — ভেতরে C কোড থাকলে compiler লাগবে, না থাকলে error. এজন্যই নতুন Python বেরোলে কিছুদিন কিছু package ইনস্টল হতে চায় না।

  2. AI-এর কনফিগে ভুল ধরুন: একটি library-র জন্য AI এই pyproject.toml দিয়েছে। অন্তত পাঁচটি সমস্যা খুঁজুন: [project]-এ name = "bd_phone", version নেই, license = {text = "MIT"}, classifiers = ["License :: OSI Approved :: MIT License"], dependencies = ["phonenumbers==8.13.0"]; আর কোনো [build-system] নেই, পাশে .github/workflows/publish.yml-এ password: pypi-AgEIcHlwaS5vcmc...
    • [build-system] নেই — frontend জানে না কোন backend; যোগ করুন, যেমন hatchling.
    • version নেই (আর dynamic-এও নেই) — metadata অবৈধ, build ব্যর্থ হবে।
    • license = {text = ...} আর License :: classifier — PEP 639-এর আগের রূপ; লিখুন license = "MIT", classifier বাদ।
    • library-তে phonenumbers==8.13.0 — হুবহু pin ব্যবহারকারীর জন্য জট; দিন phonenumbers>=8.13.
    • workflow-এ token সরাসরি লেখা — git-এর ইতিহাসে চিরকাল থাকবে। এখনই token বাতিল করুন, তারপর trusted publishing.
    • বাড়তি: requires-python নেই; name-এ bd_phone চলবে (normalization-এ bd-phone-এর সমান), তবে প্রথা অনুযায়ী bd-phone লেখা পরিষ্কার।
  3. version সাজান: taka-format-এর PyPI-তে আছে: 1.0.0, 1.0.0rc1, 1.0.0a1, 1.0.0.post1, 1.0.0b2, 1.0.0.dev3, 1.1.0b1. (ক) ছোট থেকে বড় সাজান। (খ) pip install taka-format কোনটি নেবে? (গ) pip install --pre taka-format? (ঘ) "taka-format<1.1"?

    (ক) 1.0.0.dev3 < 1.0.0a1 < 1.0.0b2 < 1.0.0rc1 < 1.0.0 < 1.0.0.post1 < 1.1.0b1.

    (খ) 1.0.0.post1 — সবচেয়ে বড় স্থিতিশীল version; post-release স্থিতিশীল ধরা হয়, pre-release নয়। (গ) 1.1.0b1 — --pre দিলে pre-release-ও বিবেচনায় আসে। (ঘ) 1.0.0.post1 — <1.1 1.1-এর pre-release (1.1.0b1)-কেও বাদ দেয়।

  4. একটি বাংলাদেশি utility package বানান: bd-phone নামে একটি package-এর জন্য is_bd_mobile(number: str) -> bool লিখুন, যা 01712345678 আর +8801712345678 দুটিকেই গ্রহণ করে (১১ অঙ্ক, শুরুতে 01, তারপর 3–9-এর একটি অঙ্ক)। তারপর বলুন src layout-এ ফাইল কোথায় বসবে, আর bdphone নামে একটি console script যোগ করতে pyproject.toml-এ কী লিখবেন।
    # src/bd_phone/__init__.py
    import re
    
    _PATTERN = re.compile(r"01[3-9]\d{8}")
    
    def is_bd_mobile(number: str) -> bool:
        number = number.strip().replace(" ", "").replace("-", "")
        if number.startswith("+880"):
            number = "0" + number[4:]
        return _PATTERN.fullmatch(number) is not None
    
    assert is_bd_mobile("01712345678") and is_bd_mobile("+8801712345678")
    assert not is_bd_mobile("01212345678") and not is_bd_mobile("0171234567")

    +880 বাদ দিয়ে সামনে 0 বসালে দুটি রূপ এক হয়ে যায়। fullmatch পুরো string মেলায়, তাই ১০ বা ১২ অঙ্ক বাদ পড়ে (regex পর্ব ৩০-এ)। কাঠামো: bd-phone/pyproject.toml, bd-phone/src/bd_phone/__init__.py, bd-phone/src/bd_phone/cli.py (ভেতরে main()), bd-phone/tests/. pyproject.toml-এ:

    [project.scripts]
    bdphone = "bd_phone.cli:main"

    তারপর python -m build, wheel খুলে দেখা, নতুন venv-এ ইনস্টল করে bdphone 01712345678 — ঠিক অংশ ৫ ও ৭-এর মতো।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমনিজের Python package প্রথমবার PyPI-তে প্রকাশের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. src layout-এ কোড আর pyproject.toml লেখা
  2. python -m build দিয়ে sdist ও wheel বানানো
  3. wheel খুলে দেখা আর নতুন venv-এ ইনস্টল করে চালানো
  4. TestPyPI-তে আপলোড করে সেখান থেকে ইনস্টল-পরীক্ষা
  5. আসল PyPI-তে প্রকাশ

Python-এর একটি wheel ফাইলের নামের শেষে py3-none-any থাকলে তার অর্থ কী?

  • শুধু Linux-এ চলে, কোনো Python version-এর শর্ত নেই
  • যেকোনো Python 3-এ, যেকোনো OS-এ চলে — ভেতরে compile করা অংশ নেই
  • package-টির কোনো dependency নেই
  • এটি একটি pre-release version

ইনস্টল করা একটি Python package-এর .dist-info ফোল্ডারের কোন ফাইলে package-এর প্রতিটি ফাইলের পথ ও hash লেখা থাকে, যা দেখে pip uninstall ফাইল মোছে?

  • METADATA
  • WHEEL
  • entry_points.txt
  • RECORD

অন্যরা ব্যবহার করবেন এমন একটি Python library-র pyproject.toml-এ requests-কে dependency হিসেবে লেখার সবচেয়ে উপযুক্ত রূপ কোনটি?

  • "requests==2.31.0"
  • "requests", কোনো শর্ত ছাড়া
  • "requests>=2.31,<3" বা "requests>=2.31"
  • dependency না লিখে README-তে বলে দেওয়া “requirements.txt থেকে ইনস্টল করুন”

PEP 639 অনুযায়ী Python প্রজেক্টের pyproject.toml-এ license লেখার আধুনিক রূপ কোনটি?

  • license = "MIT" আর license-files = ["LICENSE"]
  • license = {text = "MIT"}
  • শুধু "License :: OSI Approved :: MIT License" classifier
  • license লেখার দরকার নেই, LICENSE ফাইল রাখলেই যথেষ্ট

GitHub Actions থেকে PyPI-তে trusted publishing ব্যবহার করলে repository-র secret-এ কী জমা রাখতে হয়?

  • PyPI-র একটি project-scoped API token
  • কিছুই না — workflow-এ id-token: write অনুমতি আর PyPI-তে আগে থেকে publisher যোগ করাই যথেষ্ট
  • PyPI অ্যাকাউন্টের username আর পাসওয়ার্ড
  • 2FA-র recovery code

PyPI-তে একটি package-এর version 2.0.0 আর 2.1.0rc1 আছে। কেউ শুধু pip install দিয়ে package-এর নাম লিখলে (কোনো flag বা শর্ত ছাড়া) কোনটি ইনস্টল হবে?

  • 2.1.0rc1, কারণ সেটি সংখ্যায় বড়
  • pip দ্বিধায় পড়ে error দেবে
  • 2.0.0
  • দুটিই, পাশাপাশি

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. নিজের কোড = নিজের distribution: নাম, version আর মানক ফাইল থাকলে একই কোড সব প্রজেক্টে, দলে বা PyPI-তে — কপি-পেস্ট নয়।
  2. src layout আর একটি pyproject.toml: [build-system] + [project]; license একটি SPDX expression, requires-python দিন, কমান্ড [project.scripts]-এ।
  3. python -m build → sdist + wheel: wheel একটি zip — কোড আর .dist-info (METADATA, RECORD, entry_points). প্রতিবার খুলে দেখুন, নতুন venv-এ পরীক্ষা করুন।
  4. Version প্রতিশ্রুতি: PEP 440, pre-release নিজে থেকে আসে না, একবার আপলোড মানে চিরকাল; version এক জায়গায়; library-তে পরিসর, অ্যাপে lock.
  5. নিরাপদ প্রকাশ: TestPyPI আগে, 2FA, token কখনো git-এ নয় — সম্ভব হলে trusted publishing; কোম্পানিতে একক private index.

পরের পর্বে: আপনার package এখন অন্যের হাতে। কেউ format_taka("abc") দিলে কী হবে? ফাইল না পেলে, ইন্টারনেট না থাকলে, ভুল ইনপুট এলে প্রোগ্রাম কীভাবে সম্মানের সাথে ব্যর্থ হবে — আর library-লেখক হিসেবে কোন exception তুলবেন — try, except আর নিজের exception-এর পর্ব।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান