Poetry to UV(+ Ruff) Migration
This guide captures the changes necessary to migrate the main
development, CI, and container build path from Poetry plus
black/isort/flake8/pylint to uv plus ruff.
If you do not want to migrate to Ruff, you can skip the steps related to it and keep your existing formatter/linter.
Moving to UV without changing linting tools
Note
If you want to move to uv from poetry but you don’t want to change your linting from black/isort/flake8/pylint to ruff. Please follow the below steps and ignore the ruff documentation for the rest of this migration guideline. If you are using the AI prompt, note that it might try to migrate to ruff so make sure to review the plan/actions.
Pipeline Machinery allows you to mix and match different templates, however, this migration guideline assumes you are moving to new makefiles (python-uv.mk) which only supports uv`+`ruff and might not work for your case. To keep existing linting tools:
Override the python-do-lint make target as below:
-include python-uv.mk
# Make sure to add this after your include
PYTHON_LINE_LENGTH ?= 79## Default value piped to all linting tools
PYTHON_SWITCHES_FOR_ISORT ?=## Custom switches added to isort
PYTHON_SWITCHES_FOR_BLACK ?=## Custom switches added to black
PYTHON_SWITCHES_FOR_FLAKE8 ?=## Custom switches added to flake8
python-do-lint:
@mkdir -p build/reports;
$(PYTHON_RUNNER) isort --check-only --profile black --line-length $(PYTHON_LINE_LENGTH) $(PYTHON_SWITCHES_FOR_ISORT) $(PYTHON_LINT_TARGET)
$(PYTHON_RUNNER) black --exclude .+\.ipynb --check --line-length $(PYTHON_LINE_LENGTH) $(PYTHON_SWITCHES_FOR_BLACK) $(PYTHON_LINT_TARGET)
$(PYTHON_RUNNER) flake8 --show-source --statistics --max-line-length $(PYTHON_LINE_LENGTH) $(PYTHON_SWITCHES_FOR_FLAKE8) $(PYTHON_LINT_TARGET)
$(PYTHON_RUNNER) pylint --output-format=parseable,parseable:build/code_analysis.stdout,pylint_junit.JUnitReporter:build/reports/linting-python.xml --max-line-length $(PYTHON_LINE_LENGTH) $(PYTHON_SWITCHES_FOR_PYLINT) $(PYTHON_LINT_TARGET)
@make --no-print-directory join-lint-reports
This will allow you to use the existing makefile support with existing tools without changing linting and building that is compatible with the repository dependencies. build phase is not poetry dependant
For any questions or confusion, please contact the System team, or consult the ska-ser-bar-backend repository for a reference migration.
Note
Treat this as a toolchain migration, not just a dependency change. A useful migration is one where local setup, CI, Docker, helper commands, and contributor documentation all agree on the same path.
Target State
Before calling the migration complete, the repository should have all of the following in place:
pyproject.tomluses standard PEP 621 project metadata instead of Poetry-only metadata.uv.lockexists and is committed.local dependency installation uses
uv sync.local command execution uses
uv run.formatting and linting use Ruff.
CI uses uv-aware setup and commands.
Docker builds install dependencies with uv.
documentation and helper targets no longer direct contributors to Poetry.
Baseline
Before the migration:
packaging metadata lived under
tool.poetryinpyproject.tomldependency resolution was captured in
poetry.lockshared make targets came from
.make/python.mkthe repo-level runner default was
PYTHON_RUNNER ?= poetry runCI included
python.gitlab-ci.ymlthe Docker build installed dependencies with
poetry installlinting used
isort,black,flake8, andpylint
Representative pre-migration references:
Makefileincluded.make/python.mk.gitlab-ci.ymlincludedgitlab-ci/includes/python.gitlab-ci.ymlpyproject.tomlused[tool.poetry]
Migration Sequence
It is recommended to not perform the migration as a straight shot but to instead break it into smaller, reviewable commits. The steps below describe the recommended order. Each step is designed to be a single reviewable unit of work.
Step 1: Add uv and Ruff
Install uv if it is not already available.
See the uv installation docs for installation methods.
If the repository still uses Poetry metadata, add Ruff in the least disruptive way first:
poetry add --group dev ruff
poetry lock
If the repository already uses uv-compatible metadata, the equivalent is:
uv add --dev ruff
uv lock
Goal of this step:
Ruff is installed.
Existing workflows still work.
The repository is ready for the formatter and linter swap.
Step 2: Convert pyproject.toml to Standard Project Metadata
Move away from Poetry-specific sections such as:
[tool.poetry][tool.poetry.dependencies][tool.poetry.group.dev.dependencies][tool.poetry.group.docs.dependencies][[tool.poetry.source]]
Use standard Python sections such as:
[project] (PEP 621)
[dependency-groups] (PEP 735)
[[tool.uv.index]](uv index docs)[build-system] (PEP 517)
Typical changes:
replace
tool.poetrywith[project]move runtime dependencies into
dependencies = []move dev and docs dependencies into
[dependency-groups]replace Poetry source definitions with uv index definitions
set
requires-pythonexplicitly
If the project is packaged, also switch the build backend. For any pure Python projects (which should be the majority of cases), a working replacement is uv-build:
[build-system]
requires = ["uv-build>=0.3.0"]
build-backend = "uv_build.build"
However, if your repository uses any other languages, Hatchling is a suitable alternative:
[build-system]
requires = ["hatchling>=1.27.0"]
build-backend = "hatchling.build"
For a src/ layout, a wheel target often looks like:
[tool.hatch.build.targets.wheel]
packages = ["src/your_package_name"]
Goal of this step:
the repository no longer depends on Poetry-only metadata
uv can resolve and lock dependencies directly from
pyproject.toml
Step 3: Replace poetry.lock with uv.lock
Note
Dependency pinning policy: Pin to the major version for all post-v1
packages (>=X.Y.Z,<(X+1).0.0) because minor and patch releases should
not contain breaking changes, while still allowing the project to pick up new
features automatically.
For pre-v1 packages (0.x), semver stability guarantees do not apply, so
treat them case-by-case: 0.Y.Z is pinned to the minor
(<0.(Y+1).0), and 0.0.Z is pinned to the patch (<0.0.(Z+1)).
This mirrors the behaviour of the Poetry caret (^) operator that was
used prior to the uv migration.
Once pyproject.toml is uv-compatible, generate the uv lockfile:
uv lock
If the repository uses multiple package indexes, make sure they are declared in
pyproject.toml first, otherwise the lock step will not reflect the real
installation environment.
After a successful lock:
commit
uv.lockremove
poetry.lockif the repository is fully moving off Poetry
Goal of this step:
dependency resolution is reproducible under uv
CI and Docker can install from the same lockfile
Step 4: Switch Install and Execution Paths to uv
Every place that used:
poetry install
poetry run <command>
should be reviewed and usually replaced with:
uv sync
uv run <command>
Common replacements:
Before |
After |
|---|---|
|
|
|
|
|
|
|
|
|
|
Typical commands after migration:
uv sync --all-groups
uv run pytest
uv run python path/to/script.py
uv run uvicorn package.app:app --reload
Apply this review to:
Makefiletargetsshell scripts
local helper commands
token, debug, and admin utilities
test runners
Goal of this step:
developers no longer need Poetry for local setup or execution
helper paths use the same runner as test and CI paths
Step 5: Switch Format and Lint to Ruff
Treat this as a toolchain migration, not just a package addition.
Map the old toolchain to Ruff like this:
black->ruff formatisort->ruff checkimport rules (rule setI)flake8->ruff check(rule setE,F,W)a significant subset of
pylint->ruff check(rule setPL)
Add a minimal Ruff configuration to pyproject.toml:
[tool.ruff]
line-length = 79
[tool.ruff.lint]
select = ["E", "F", "W", "I", "PL"]
Adjust select to match the level of strictness the repository had before.
The rule prefixes above cover the same ground as flake8, isort, and the
most common pylint checks.
Typical commands:
uv run ruff format .
uv run ruff format --check .
uv run ruff check .
If the repository already has shared lint targets in Make or CI templates, update those targets rather than only documenting raw CLI commands.
Goal of this step:
a single formatter and linter entrypoint is in place
local and CI lint paths point to Ruff
Step 6: Run Formatting as a Dedicated Change
When switching from black and isort to Ruff, expect a large mechanical
diff. Keep that isolated from functional changes.
Typical command:
uv run ruff format src tests
Why isolate this:
it makes code review realistic
it separates formatting churn from real migration breakage
it makes later blame and history easier to interpret
Goal of this step:
the codebase is normalized to the new formatter
formatter-related failures stop showing up as migration noise
Step 7: Update CI to Use uv-Aware Setup
CI needs the same migration as local development.
Pipeline machinery uses UV. Here are the new CI includes and steps:
include:
# Python
- project: 'ska-telescope/templates-repository'
file: 'gitlab-ci/includes/python-uv.gitlab-ci.yml'
Goal of this step:
CI uses the same dependency manager, lockfile, and execution model as local development
Step 8: Update Docker to Install Dependencies with uv
If the repository builds a container image, switch the dependency-install layer to uv.
Please see the example Docker in Container Base images for the recommended pattern.
Goal of this step:
container builds are aligned with uv locking and installation
Step 9: Validate and Fix Breakage Exposed by the New Runner
Do not assume the migration is purely mechanical. Toolchain changes often surface pre-existing assumptions.
Typical validation commands:
uv sync --all-groups
uv run pytest
uv run ruff format --check .
uv run ruff check .
uv build
Also validate:
local startup
token, debug, and admin helper scripts
integration test entrypoints
Docker builds
Expect a small number of functional fixes. In the repository this guide is based on, the migration exposed a configuration assumption in a token issuance path and required a small code fix.
Goal of this step:
the repository is not just converted on paper
the repository actually runs, tests, lints, and builds under uv and Ruff
Step 10: Update Docs and Helper Targets Last
The migration is not done until the docs stop teaching the old workflow.
Update:
README.mdlocal agent and developer notes
onboarding documentation
Make targets
shell snippets in scripts or wiki pages
Typical replacements:
poetry install --with dev->uv sync --all-groupspoetry run uvicorn ...->uv run uvicorn ...formatter and linter docs -> Ruff commands
Goal of this step:
new contributors follow the new path by default
there is no accidental regression back to Poetry
Repository-Specific Changes
The following were the main repository-level changes in the migration this guide is based on:
Area |
Change |
|---|---|
Package metadata |
|
Locking |
|
Build backend |
|
Package indexes |
Poetry sources were replaced by |
Shared Make support |
|
Default runner |
|
CI template |
GitLab CI switched from the generic Python include to the uv-aware one. |
Docker |
image builds switched from |
Formatting and linting |
the active path moved to |
Formatting Churn Versus Functional Change
Mostly formatting churn:
the initial
ruff formatpass typically reformats many files at oncemost Python source and test diffs will be line wrapping and whitespace normalization from that one commit
Functional or workflow changes:
project metadata and build backend conversion
lockfile replacement
Make and
.makerunner changesCI template switch
Docker install path switch
functional fixes exposed by the new runner (e.g. configuration fallbacks)
Non-Formatting Code Changes
Most Python source changes in a migration will be from ruff format, but
switching the runner can also expose pre-existing bugs or configuration
assumptions. In the repository this guide is based on, for example:
a token issuance function assumed a configuration value was always set
the uv-based test path ran in a slightly different environment and exposed the missing fallback
a small defensive code change was needed to handle the case gracefully
Why this matters:
the uv-based test path can surface configuration assumptions that Poetry masked
these are real bugs, not migration artefacts
budget time for a small number of functional fixes alongside the migration
Residual Poetry References Clean Up
The migration may still leave follow-up cleanup behind. In the repository this guide is based on, the remaining work was:
Area |
Current state |
Why it matters |
|---|---|---|
|
still exported |
leftover Poetry-specific CI configuration |
dev dependency group |
still included |
legacy tools remained even though Make lint and format paths used Ruff |
Recommended Follow-Up Cleanup
Remove stale Poetry-specific CI variables.
Decide whether legacy formatter and linter packages should remain for compatibility or be removed.
Make sure helper targets, local startup, and token utilities all use
uv runconsistently.Check that contributor-facing documentation only teaches the uv and Ruff workflow.
What to Keep, What to Remove
You do not need to remove all legacy tools on day one if that makes the migration riskier. A practical path is:
switch the active workflow to uv and Ruff first
keep legacy packages temporarily if they are still referenced somewhere
remove them once CI, docs, and helper paths are clean
Do not leave the repository in an ambiguous state forever. By the end of the migration there should be one documented path.
One-Shot AI Migration Prompt
Full migration prompt (copy/paste into your AI coding agent)
You are migrating this repository from Poetry + black/isort/flake8/pylint
to uv + ruff in one end-to-end pass.
Operate directly on the repository in my workspace and complete the
migration without stopping for confirmation unless you are blocked by
missing credentials or required environment access.
Follow this contract:
- Inputs: existing repository files, current CI setup, Docker build path,
and contributor docs. Ignore submodules and .make folders. Add the latest version
of uv and ruff to the repo if they are not already present. This can be found at
https://pypi.org/project/uv/ and https://pypi.org/project/ruff/.
- Outputs: a fully migrated repo with uv/ruff as the single documented
and operational path.
- Success criteria: local setup, lint, test, build, CI config, Docker,
and docs all align on uv/ruff and pass verification.
- Error mode: if a step fails, diagnose root cause, apply focused fixes,
and re-run checks (up to 3 targeted iterations per failure).
Required migration scope:
0) Before proceeding, ask the user if they would like to migrate to Ruff too. If they do not,
skip all Ruff-related steps and keep the existing formatter and linter. Make sure to add this after pyton-uv.mk include in the Makefile:
PYTHON_LINE_LENGTH ?= 79## Default value piped to all linting tools
PYTHON_SWITCHES_FOR_ISORT ?=## Custom switches added to isort
PYTHON_SWITCHES_FOR_BLACK ?=## Custom switches added to black
PYTHON_SWITCHES_FOR_FLAKE8 ?=## Custom switches added to flake8
python-do-lint:
@mkdir -p build/reports;
$(PYTHON_RUNNER) isort --check-only --profile black --line-length $(PYTHON_LINE_LENGTH) $(PYTHON_SWITCHES_FOR_ISORT) $(PYTHON_LINT_TARGET)
$(PYTHON_RUNNER) black --exclude .+\.ipynb --check --line-length $(PYTHON_LINE_LENGTH) $(PYTHON_SWITCHES_FOR_BLACK) $(PYTHON_LINT_TARGET)
$(PYTHON_RUNNER) flake8 --show-source --statistics --max-line-length $(PYTHON_LINE_LENGTH) $(PYTHON_SWITCHES_FOR_FLAKE8) $(PYTHON_LINT_TARGET)
$(PYTHON_RUNNER) pylint --output-format=parseable,parseable:build/code_analysis.stdout,pylint_junit.JUnitReporter:build/reports/linting-python.xml --max-line-length $(PYTHON_LINE_LENGTH) $(PYTHON_SWITCHES_FOR_PYLINT) $(PYTHON_LINT_TARGET)
@make --no-print-directory join-lint-reports
1) Convert pyproject metadata
- Replace Poetry-only sections with standard metadata:
[project], [dependency-groups], [build-system], and [[tool.uv.index]]
where needed.
- Set requires-python explicitly.
- If build backend is poetry-core, switch to a suitable backend
(prefer uv-build for pure Python projects, Hatchling where needed).
2) Replace lockfile and dependency flow
- Generate and commit uv.lock.
- Remove poetry.lock once migration is complete.
- Ensure installs use uv sync (and uv sync --all-groups where dev/docs
dependencies are required).
3) Replace command execution path
- Replace poetry run usages with uv run across Makefiles, scripts,
helper utilities, docs snippets, and CI jobs.
- Ensure repo-level runner defaults point to uv run.
4) Migrate lint/format toolchain to Ruff
- Add/update [tool.ruff] config in pyproject.toml.
- Map old behaviour:
black -> ruff format
isort -> ruff check (I)
flake8 -> ruff check (E,F,W)
pylint subset -> ruff check (PL)
- Update lint/format targets in Make and CI to use Ruff.
5) Keep formatting churn isolated when possible
- If practical, isolate mass formatting changes from functional
migration edits; if not practical in one pass, clearly separate
commits or file groups in your summary.
6) Update CI/CD
- Remove Poetry bootstrap/config remnants.
- Switch includes/templates/steps to uv-aware paths.
- Replace poetry install and poetry run commands with uv equivalents:
include:
# Python
- project: 'ska-telescope/templates-repository'
file: 'gitlab-ci/includes/python-uv.gitlab-ci.yml'
- Ensure CI runs uv sync, uv run ruff check, uv run pytest, and uv build
(or equivalent project build command).
7) Update Docker build path
- Replace poetry install layers with uv sync-based layers.
- Use this Dockerfile as a reference:
.. code:: Dockerfile
FROM artefact.skao.int/ska-build-python:0.5.0 AS requirements
WORKDIR /src
COPY uv.lock pyproject.toml ./
RUN uv sync --frozen --no-dev --no-install-project
FROM artefact.skao.int/ska-python:0.2.5
WORKDIR /src
ENV VIRTUAL_ENV=/src/.venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY --from=requirements ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY ./src/my_project ./my_project
#Add source code to the PYTHONPATH
#so python is able to find our package
#when we use it on imports
ENV PYTHONPATH=${PYTHONPATH}:/src/
...
- Preserve reproducibility (e.g., --frozen and lockfile-driven install)
and keep runtime image free of unnecessary dev deps.
8) Update documentation and contributor paths
- Update README, onboarding/contributor docs, and internal how-to pages
so they teach only uv/ruff.
- Remove or flag stale Poetry-era instructions.
9) Validate end-to-end and fix breakage
- Run and pass (or equivalent):
uv sync --all-groups
uv run ruff format --check .
uv run ruff check .
uv run pytest
uv build
- Also validate helper scripts and local startup paths that previously
relied on poetry run.
- If migration exposes functional bugs, fix them and include a short
rationale.
10) Final quality gate report
- Provide a concise summary with:
* files changed and why
* any deferred items and rationale
* PASS/FAIL for Build, Lint/Typecheck, and Tests
* explicit confirmation that Poetry is no longer the active path
in code, CI, Docker, and docs.
Additional constraints:
- Use minimal, targeted edits; avoid unrelated refactors.
- Preserve existing project conventions.
- Do not leave the repo in a dual-path ambiguous state.
- If something cannot be automated, clearly state what is blocked, why,
and the exact manual follow-up required.