وقتی uv init روی ماشینی که پایتون 3.14 دارد پروژه می‌سازد، خط requires-python = ">=3.14" را داخل pyproject.toml می‌نویسد. همین یک خط، پروژه را روی هر ماشین دیگری که پایتون 3.11 یا 3.12 دارد غیرقابل نصب می‌کند و حتی فرمان uv python pin هم آن را اصلاح نمی‌کند. در این پست با یک پروژه‌ی واقعی نشان می‌دهیم این گیر کجا می‌افتد، چرا رفعش از همان فرمان ممکن نیست، و چگونه پروژه را پیش از اولین commit برای چند نسخه‌ی پایتون باز کنیم. لحظه‌ی خواندن همه‌ی عددها: ۱۳ مهر ۱۴۰۵، برابر با ۵ اکتبر ۲۰۲۶.

نصب uv و ساخت اولین پروژه

خود uv یک مدیر بسته و پروژه‌ی پایتون است که به زبان راست نوشته شده و هر کار را خودش انجام می‌دهد: محیط مجازی می‌سازد، نسخه‌ی پایتون را مدیریت می‌کند، وابستگی‌ها را قفل می‌کند و اجرا می‌کند. لحظه‌ی خواندن این پست، آخرین نسخه‌ی منتشرشده روی PyPI نسخه‌ی 0.12.23 است که در ۳ اکتبر ۲۰۲۶ منتشر شده و ریپوی آن با مجوز Apache-2.0 و بیش از ۹۰ هزار ستاره میزبانی می‌شود.

نصب از راه نصب‌کننده‌ی مستقل، یک خط است. روش‌های دیگر هم هستند و همه در صفحه‌ی نصب رسمی فهرست شده‌اند: pip install uv، brew install uv و در ویندوز winget install --id=astral-sh.uv -e. نسخه‌ی دلخواه را هم می‌توانید در نشانی بگذارید.

# نصب مستقل؛ برای نسخه‌ی دلخواه 0.12.22 را در نشانی بگذارید
$ curl -LsSf https://astral.sh/uv/install.sh | sh

# ساخت پروژه و ورود به آن
$ uv init hoosh-demo
Initialized project `hoosh-demo`

# اولین اجرا: همین یک فرمان محیط مجازی و فایل قفل را می‌سازد
$ cd hoosh-demo
$ uv run hello-world
Hello from hoosh-demo!

آنچه uv init می‌سازد بیش از یک pyproject.toml است. بر اساس مستند ساخت پروژه، از نسخه‌ی 0.12 به بعد برای پروژه‌های کاربردی هم به‌صورت پیش‌فرض یک build system تعریف می‌شود و کد در src/<project_name>/ می‌نشیند. نتیجه‌ی واقعی روی این ماشین چهار بخش داشت:

بخشکارش چیست
pyproject.tomlمتادیتای پروژه، وابستگی‌ها و خط requires-python
.python-versionنسخه‌ی پیش‌فرض پایتون پروژه؛ روی این ماشین 3.14
uv.lockنسخه‌های دقیق وابستگی‌ها؛ باید در گیت commit شود
.venv/محیط مجازی که uv sync پر می‌کند

ساخت محیط مجازی را هم زمان‌سنجی کردم. برای همان محیط خالی، python3 -m venv روی این ماشین ۴٫۰۳ ثانیه طول کشید و uv venv کمتر از ۰٫۰۱ ثانیه. این دو عدد از یک اجرای زمان‌سنجی‌شده روی همین سرور آمده‌اند و برای مقایسه‌ی سرعت روی سخت‌افزار دیگر معتبر نیستند.

خطایی که uv خودش می‌سازد

در پروژه‌ای که بالا ساختیم، خط requires-python را خود uv init بر اساس پایتون موجود روی همان ماشین پر کرد، نه بر اساس تصمیمی که ما گرفته بودیم. نتیجه در فایل واقعی این بود:

$ cat pyproject.toml
[project]
name = "hoosh-demo"
version = "0.1.0"
readme = "README.md"
requires-python = ">=3.14"      # از پایتون همین ماشین آمد، نه از تصمیم ما
dependencies = [
    "mcp>=2.3.0",
]

[build-system]
requires = ["uv_build>=0.12.21,<0.13.0"]
build-backend = "uv_build"

حالا همین پروژه را به ماشینی ببریم که پایتون 3.11 دارد. اول مفسر را با خود uv نصب می‌کنیم، که در ۲٫۰۶ ثانیه انجام شد و ۲۹٫۴ مگابایت دانلود کرد.

$ uv python install 3.11
Downloading cpython-3.11.16-linux-x86_64-gnu (29.4MiB)
Installed Python 3.11.16 in 2.04s
 + cpython-3.11.16-linux-x86_64-gnu (python3.11)

# حالا هم‌تیمی با پایتون 3.11 می‌خواهد پروژه را همگام کند
$ uv sync --python 3.11
Using CPython 3.11.16
error: The requested interpreter resolved to Python 3.11.16, which is
incompatible with the project's Python requirement: `>=3.14`
(from `project.requires-python`)

پیام خطا دقیق است و اسم فایل را می‌آورد، اما راه‌حلش در همان فایلی است که uv خودش ساخته بود. مستند نسخه‌های پایتون می‌گوید فرمان uv python pin فایل .python-version را می‌سازد تا مشخص کند محیط مجازی با کدام نسخه ساخته شود. اما این فرمان هم نتوانست از شرط requires-python عبور کند:

$ uv python pin 3.11
error: The requested Python version `3.11` is incompatible with the
project `requires-python` value of `>=3.14`.

$ cat .python-version
3.14      # دست‌نخورده ماند

این تنگ‌ترین لحظه‌ی کار با uv است: فرمانی که دقیقا برای همین کار ساخته شده، چون شرط اولیه را ناسازگار می‌بیند، اجازه‌ی اصلاح آن را نمی‌دهد. ترتیب درست کار این است که اول requires-python را باز کنیم و بعد pin کنیم. پس از ویرایش همان یک خط در pyproject.toml، همان فرمان بدون اعتراض اجرا شد:

$ uv python pin 3.11
Updated `.python-version` from `3.14` -> `3.11`

$ uv lock
Using CPython 3.11.16
Resolved 31 packages in 3.41s

$ uv run python -c "import sys, mcp; print(sys.version.split()[0])"
3.11.16

عدد ۳۱ بسته در این مرحله فقط با mcp بود. uv sync محیط را دور انداخت و دوباره ساخت، چون مفسر عوض شده بود، و همین را در خروجی گفت: Removed virtual environment at: .venv. این رفتار درست است، چون محیطی که با 3.14 ساخته شده بود برای 3.11 بی‌فایده است.

ابزار یک‌بارمصرف با uvx و نصب دائمی با uv tool

بخش دیگری از کار uv بیرون از پروژه است: اجرای یک ابزار پایتونی بدون نصب در محیط پروژه. uvx ابزار را در یک محیط موقت می‌سازد و اجرا می‌کند. روی این ماشین، دانلود و اجرای ruff با uvx در مجموع ۱۰٫۶ ثانیه طول کشید که ۹٫۹ مگابایت دانلود و ۰٫۸ میلی‌ثانیه نصب را شامل می‌شد؛ اجرای دوم ۰٫۰۲ ثانیه بود چون بسته در کش بود.

# اجرای یک‌بارمصرف: نه نصب دائمی، نه اثری در PATH
$ uvx ruff --version
ruff 0.16.10

# روی یک فایل واقعی، و این بار کد خروج ۱ دارد چون خطا پیدا شد
$ uvx ruff check t.py
I001 [*] Import block is un-sorted or un-formatted
F401 [*] `os` imported but unused
Found 2 errors.
[*] 2 fixable with the `--fix` option.

اگر ابزار را زیاد صدا می‌زنید، نصب دائمی مناسب‌تر است. uv tool install ابزار را در محیطی جدا از پروژه نگه می‌دارد و فرمانش را در PATH می‌گذارد. در این آزمایش هر دو حالت از یک کش گرم استفاده کردند، پس زمان نصب دائمی ۰٫۰۲ ثانیه شد؛ در اولین اجرا هزینه‌ی دانلود یک‌بار پرداخت می‌شود. مستند ابزارها تفاوت این دو را دقیق‌تر توضیح می‌دهد.

$ uv tool install ruff
Resolved 1 package in 3ms
Installed 1 executable: ruff

$ uv tool list
ruff v0.16.10
- ruff

$ uv tool uninstall ruff
Uninstalled 1 executable: ruff

تفاوت عملی این است که uvx محیط پروژه‌ی شما را کثیف نمی‌کند و پس از اجرا چیزی باقی نمی‌ماند، ولی uv tool install یک ورودی دائمی به PATH اضافه می‌کند که باید بعدا پاکش کنید. کش پس از همه‌ی این کارها ۲۱۸ مگابایت شد و مسیرش را uv cache dir می‌دهد.

قفل فایل و خط فرمان CI

بخشی که پروژه‌های تیمی را نگه می‌دارد، فایل uv.lock است. بر اساس مستند قفل و همگام‌سازی، همگام‌سازی در uv خودکار است و uv run پیش از اجرا هم قفل را به‌روز و هم محیط را بررسی می‌کند. همین رفتار یک مشکل واقعی دارد: در خط لوله‌ی ci می‌خواهید نسخه‌ها هرگز بی‌سروصدا عوض نشوند، پس باید این خودکار بودن را خاموش کنید.

برای نشان دادنش، یک وابستگی به pyproject.toml اضافه کردم بدون آنکه قفل را به‌روز کنم. هر دو حالت درست، قفل کهنه را رد کردند:

$ uv sync --locked
Resolved 34 packages in 3.39s
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.
hint: To update the lockfile, run `uv lock`.

$ uv lock --check
Resolved 34 packages in 17ms
error: The lockfile at `uv.lock` needs to be updated, but `--check` was provided.

پس از uv lock، همان فرمان‌ها سبز شدند و uv lock --check در ۱ میلی‌ثانیه پاسخ داد. مستند ساختار پروژه توضیح می‌دهد چرا باید uv.lock را در گیت commit کنید و این فایل را نباید دستی ویرایش کرد. uv export هم قفل را برای ابزارهایی که قفل را نمی‌فهمند به requirements.txt تبدیل می‌کند و منشأ هر بسته را در همان فایل نگه می‌دارد.

$ uv export --format requirements-txt --no-hashes
# This file was autogenerated by uv via the following command:
#    uv export --format requirements-txt --no-hashes
-e .
annotated-types==0.8.0
    # via pydantic
anyio==4.15.1
    # via
    #   httpx
    #   mcp

قاعده‌ای که این آزمایش‌ها بیرون دادند

پس از این همه فرمان، یک قاعده‌ی عملی باقی می‌ماند: uv init را روی ماشینی اجرا نکنید که پایتونش از بالاترین نسخه‌ای که واقعا قصد پشتیبانی از آن را دارید بالاتر است. اگر پروژه باید روی چند نسخه‌ی پایتون کار کند، requires-python را در همان اولین commit درست کنید، چون بعد از آن هر اصلاح یک commit جداگانه می‌خواهد و uv python pin هم تا آن زمان کار شما را انجام نمی‌دهد.

برای دیدن وضعیت پروژه، سه فرمان کافی است و هر سه بدون نیاز به شبکه کار می‌کنند. اول uv tree --depth 1 درخت وابستگی‌ها را نشان می‌دهد، بعد uv lock --check می‌گوید قفل با متادیتا هم‌خوان است یا نه، و uv sync --locked تضمین می‌کند آنچه روی دیسک نصب شده دقیقا همان چیزی است که در قفل نوشته شده.

$ uv tree --depth 1
hoosh-demo v0.1.0
├── httpx v0.28.1
└── mcp v2.3.0

$ uv sync --locked
Resolved 34 packages in 1ms
Checked 29 packages in 0.22ms

اگر پروژه‌ی شما یک سرور MCP دارد، روش کامل‌تر ساخت سرور با همین ابزار در پست ساخت سرور و کلاینت با SDK نسخه‌ی 2 پایتون آمده است. آنجا هم یک مهاجرت واقعی بود که با همین نگاه به نسخه‌های قفل‌شده پیدا می‌شد: بسته‌ی mcp نسخه‌ی ۲٫۳٫۰ در این آزمایش مسیر mcp.server.fastmcp را نداشت و خودش راهنمای مهاجرت را در پیام خطا می‌داد.

مستندات دیگری که در این آزمایش به آن‌ها تکیه شد، راهنمای کار با پروژه و مرجع خط فرمان بود. نسخه‌ی نصب‌شده روی این سرور 0.12.21 بود و بیشتر عددهای این پست از همین اجراهای محلی آمده‌اند؛ روی سخت‌افزار و نسخه‌ی دیگری این زمان‌ها فرق می‌کنند.

منابع

  1. ریپوی رسمی uv در گیت‌هاب؛ آمار ستاره‌ها و تاریخ آخرین push، لحظه‌ی خواندن: ۵ اکتبر ۲۰۲۶
  2. مستند نصب uv؛ خط نصب مستقل و روش‌های جایگزین، بازبینی: ۵ اکتبر ۲۰۲۶
  3. راهنمای کار با پروژه در uv؛ فهرست فایل‌های ساخته‌شده توسط uv init، بازبینی: ۵ اکتبر ۲۰۲۶
  4. ساخت پروژه در uv؛ رفتار build system پیش‌فرض از نسخه‌ی 0.12، بازبینی: ۵ اکتبر ۲۰۲۶
  5. نسخه‌های پایتون در uv؛ نقش فایل .python-version و ترتیب اولویت نسخه‌ها، بازبینی: ۵ اکتبر ۲۰۲۶
  6. قفل و همگام‌سازی در uv؛ معنای پرچم‌های locked و frozen، بازبینی: ۵ اکتبر ۲۰۲۶
  7. ساختار و فایل‌های پروژه در uv؛ نقش uv.lock و پوشه‌ی .venv، بازبینی: ۵ اکتبر ۲۰۲۶
  8. ابزارها در uv؛ تفاوت uvx و uv tool، بازبینی: ۵ اکتبر ۲۰۲۶
  9. مرجع خط فرمان uv؛ فهرست کامل زیرفرمان‌ها، بازبینی: ۵ اکتبر ۲۰۲۶
  10. صفحه‌ی بسته‌ی uv در PyPI؛ نسخه‌ی 0.12.23 و تاریخ انتشار، بازبینی: ۵ اکتبر ۲۰۲۶