Migrating from Poetry to UV in a Python Project

Jul 19, 2025 min read

This is a very short post about our move from Poetry to UV in the Sudoku Solver we’re building in Python. I don’t have a definitive reason for switching, but I’ve been experimenting with UV recently and I’m starting to prefer it over Poetry. In this post, we’ll outline the goals of the migration, the steps we took, and the outcome. If you find this informative, you may also benefit from my previous post about Python package managers in 2025.

The current code at the time of writing can be found on GitHub where you can also see the differences since the last blog post.

If you like this post and you’d like to know more about how to plan and write Python software, check out the Python tag. You can also find other posts in the Sudoku Series.


Aims of the Migration

Before embarking on a complex process such as switching package managers, it’s a good idea to set out your aims. These are mostly around not losing functionality and identifying where enhancements could be made to the process.

  • Keep the installation of dependencies simple.
  • Ensure the system can still be run after the transition.
  • Ensure our tests still pass.
  • See if there are any uv specific techniques that we can apply.

Steps to Migrate

I wanted to keep things simple. Poetry was using a pyproject.toml file and so does UV. So here’s what I did to get everything moved over to UV.

  • Rename pyproject.toml to oldpyproject.toml to keep it as a reference.
  • Delete any Poetry specific lock files and virtual environment directories.
  • Run uv init in the repository to generate some initial files, including:
    • A new pyproject.toml.
    • A .python-version file.
  • Update the project description in the pyproject.toml file.
  • Add dependencies back to the project.
  • Set up a build environment and move to a package code layout.
  • Add a project script to run the solver.
  • Update our linting script with UV specific calls.

Let’s go into more detail on the last few steps.


Adding Dependencies

Although we have no core project dependencies yet, if we did we could add them to the uv managed environment using the following:

uv add package-name

We do have some dev dependencies though, which we can install as follows:

uv add --dev mypy pytest ruff

This in turn updates the pyproject.toml file with these dependencies, creates the uv.lock file and generates a new .venv directory with the packages included.


Define a Build Environment with setuptools

It would be nice if we could continue to run the solver with a custom script name instead of having to call python on main.py or something similar. This is possible in uv and we need to define a build environment. By adding the following section to the pyproject.toml, we tell uv to use that for building our code and in turn it treats our project as a package with modules.

[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

To get the most from this arrangement, we need to move our source code and tests into a standard package structure. So we ensure our application files are under a src sub-directory, which they already are, and we move our tests from last time into a test directory.


Creating a Script Command

Now we can create a custom script to run our solver by adding the following to our pyproject.toml file.

[project.scripts]
sudoku-solver = "solver:run"

At the same time, I’ve renamed the awkwardly named __main__.py file to runner.py and renamed the main() method to run() to match this script definition.

This allows us to build and run the application using the simple command:

uv run sudoku-solver

This automatically ensures the environment is up-to-date, then it runs our Python code.


Update Linting to Use UV and UVX

The lint.sh file has references to Poetry in it and that’s no longer the right way to call the linters. So let’s make some updates:

uv run mypy .

This ensures the project environment is active, which is necessary for mypy to resolve imports correctly.

uvx ruff check
uvx ruff format

These run ruff in an isolated tool environment, which avoids unexpected side effects from project-level configuration.

You cannot use the uvx command for mypy because, without the project environment, you cannot parse the imports and other Python specific features. A separate environment for mypy causes the analysis to fail.


Verify Everything Works with Tests

As discussed last time, the litmus test is to ensure your tests still work. We do this by executing the command:

uv run pytest

We are greeted with a full suite of passing tests, which means we can rest easy that we haven’t broken any logic while updating the package manager:

configfile: pyproject.toml
collected 21 items

tests/model/test_cell.py ..............                   [ 66%]
tests/model/test_grid.py .......                          [100%]

====================== 21 passed in 0.01s ======================

Wrapping up

This was a relatively simple change overall, and while not strictly necessary, it’s helping align the project with tooling I’m more comfortable with going forward. UV not only achieves all the same functionality we had with Poetry on this project; it also provides us useful features for the future, such as the ability to load environment variables from a file without the need for python-dotenv.

I hope this gives you the confidence and helps you to migrate your projects as well. If this has been useful to you, please share it with others in your tech groups.

Last Updated: Jul 19, 2025