Setting up a Python Project Using asdf, PDM, and Ruff
Explore a modern Python project setup with asdf for runtime management, PDM for dependency handling, and Ruff for linting and formatting.
When I was tasked with looking into alternative ways to set up a new Python project (not just using the good ol' pip
and requirements.txt
setup), I decided to try to find the tools that felt best to me, as someone who writes Python and Ruby. On this journey, I found a way to manage dependencies in Python that felt as good as bundler, among other great tools.
The Runtime Version Manager #
asdf has been my primary tool of choice for language version management for multiple years now. The ease of adding plugins and switching between versions of those plugins at a local or global level has saved me massive amounts of time compared to alternatives.
If you've never set up asdf before, follow the instructions here to get it set up. For reference, I use fish for my shell, so I installed asdf using the "Fish & Git" section.
Once you have asdf on your machine, the next step is to add the plugins you need for your project. Plugins are the actual tools that you want to manage the versions of, like NodeJS, Python, Ruby, etc. For the purposes here, I'll start with adding the plugin for Python:
asdf plugin-add python
Once you have added a plugin to asdf, you're ready to install various versions of that plugin. Since we just installed Python, we can install the version we want:
asdf install python 3.12.4
# OR if we want to just use whatever the latest version is
asdf install python latest
Once the version you want is installed, you can tell asdf to use that version in the current directory by running:
asdf local python 3.12.4
# OR
asdf local python latest
depending on which version of python you installed.
The Dependency Manager #
In the past, I just used pip install
and requirements file(s) to handle most of this. I knew of other options, like pipx
or pipenv
, but I still have never tried using them. I was more interested in finding a dependency manager that did these things in a significantly different way than what I was used to with pip
.
Therefore, I wanted to find something that felt similar to bundler for Ruby. Luckily, very early on in my journey here, I found PDM.
Upon reading what PDM did, I immediately decided to try it out and get a feel for what it offered. Some key notes for me that piqued my interest:
- Lockfile support
- Can run scripts in the "PDM environment"
pdm run flask run -p 3000
executes the normalflask run -p 3000
command within the context of your installed packages with PDM.- In other words, it adheres to PEP 582 and allows you to run project commands without needing to be in a virtual environment, which to me is a big plus.
- Similar commands to bundler
pdm run
=>bundle exec
pdm install
=>bundle install
pdm add <package>
=>bundle add <gem-name>
- Note: My workflow was almost always to just add
gem <gem-name>
to theGemfile
rather than usingbundle add
, but there is no direct 1:1 equivalent of aGemfile
with PDM.
- Note: My workflow was almost always to just add
Installing PDM #
PDM has its own asdf plugin, so let's just use that here as well! Running:
asdf plugin-add pdm
adds the plugin itself to asdf, and running:
asdf install pdm latest
# can replace 'latest' with a specific version number here too
installs the latest version of PDM. Finally, set the local version with:
asdf local pdm latest
✦ | Side note about asdf local | |
asdf local creates a .tool-versions file (if it doesn't already exist) in the current working directory, and appends the plugin and version number to it. At this point, the directory in which you ran asdf local python 3.12.4 and asdf local pdm latest should have that .tool-versions file, and the contents should be a line each for Python and PDM with their associated version numbers. This way, if someone else pulls down your project, they can just run asdf install and it will install the versions of those plugins, assuming the user has the necessary plugins added themselves. |
Now that we have PDM and Python set up, we're ready to use PDM to install whichever packages we need. For simplicity, let's set up a simple Flask app:
pdm add flask flask-sqlalchemy flask-htmx
This line adds Flask, Flask-SQLAlchemy and Flask HTMX. Flask is a web application framework, Flask-SQLAlchemy adds SQLAlchemy and its ORM, and HTMX builds on top of HTML to allow you to write more powerful HTML where you'd otherwise need some JS. Side note, but HTMX is really cool. If you haven't used it before, give it a go! I'm even a part of the exclusive group of HTMX CEOs.
Linting and Formatting #
Finally, I wanted to find a way to avoid pulling in multiple packages (commonly, Black, Flake8 and isort) to handle linting and formatting, which felt to me like it could be the job of one tool.
Pretty quickly I was able to find Ruff which did everything I wanted it to, along with being really fast (thanks Rust 🦀).
First things first, we need to install Ruff. Since it's a Python package, we can do it using PDM:
pdm add ruff
Once it's installed, we can use ruff check
and ruff format
to lint and format, respectively. Note that since we installed via PDM, we need to prepend those ruff calls with pdm run
:
pdm run ruff check --fix
This runs the linter and fixes any issues found (if they are automatically fixable). The linter can also be run in --watch
mode:
pdm run ruff check --watch
which re-lints on every saved change and tells you of any new errors it finds.
The Ruff formatter is similar to use:
pdm run ruff format
which will automatically fix any formatting issues that it finds and can fix. If you want to use this in CI (which you should), you can use the --check
flag that will instead exit with a non-zero status code, rather than actually formatting the files:
pdm run ruff format --check
Bringing it all together #
Working with projects set up this way is much easier than how I used to do it. Using tools like asdf, PDM, and Ruff rather than pyenv, pip, and Black/Flake8/isort make both setting up projects and pulling down/installing existing projects more straightforward. I hope the contents of this article are helpful to anyone interested in setting up Python projects in a similar way.