I've recently changed my python project workflow based on a great write-up by Trey Hunner.
Trey did a great job of explaining the details of what uv
is and why you might want to use it, and also set up some scripts for zsh to recreate a workflow that is quite similar to what I've been using for several years.
I have shamelessly copied his work, ported it to bash ('cause I'm old-school) and expanded it a bit.
The Workflow
Before going into the details of how to set up this system, I'll walk you through what it looks like in operation. There are three commands, one used when creating a new project, one used in normal, day-to-day operations, and one primarily for debugging things.
To set up a new project in this workflow, you first need to cd
to the project's directory. It doesn't matter if it already exists, usually for me it does, but it can be an empty directory, too.
Once you're in the project directory, you then use the venv
command (which is shown below in a custom bash script) to create the virtual environment, set up direnv
to use this directory, and install the requirement.txt
file if it's present.
The venv
script also does a little bookkeeping in the background to help with the second command, the day-to-day one, workon
.
To use the workon
command (again, shown in the next section), you simply type workon
followed by the name of the project. The command will change to you to project's directory and activate the virtualenvironment for you.
Finally, for completeness (and to help me debug things while setting this up), there is the rmvenv
which attempts to remove all traces left by the venv
command.
The Setup
To get this workflow to operate, you will need to install uv
and direnv
.
From there you can add the code snippet below to your .bashrc
file (or however you like to have these things) and source that file.
In his article, Trey also goes into detail about using starship
which I also use, but I feel that, while that's pretty cool, it's not completely necessary to getting this workflow running.
If you're interested in making your prompt look cool with python venv and version info, I'd recommand starship and Trey's article referenced above.
Without further ado, here's the code, which can also be found here:
venv() {
local venv_name
local projects_file="$HOME/.projects"
local dir_name=$(basename "$PWD")
# If there are no arguments or the last argument starts with a dash, use dir_name
if [ $# -eq 0 ] || [[ "${!#}" == -* ]]; then
venv_name="$dir_name"
else
venv_name="${!#}"
set -- "${@:1:$#-1}"
fi
# Check if .envrc already exists
if [ -f .envrc ]; then
echo "Error: .envrc already exists" >&2
return 1
fi
if grep -Fq ${venv_name} ${projects_file}; then
echo "Error: a project named ${venv_name} already exists" >&2
return 1
fi
# Create venv
if ! uv venv --seed --prompt "$venv_name" "$@" .venv; then
echo "Error: Failed to create venv" >&2
return 1
fi
source .venv/bin/activate
# Create .envrc
echo "layout python" > .envrc
# Append project name and directory to projects file
echo "${venv_name} = ${PWD}" >> $projects_file
# Allow direnv to immediately activate the virtual environment
direnv allow
if [ -f requirements.txt ]; then
# Install requirements if requirements.txt exists
pip install -r requirements.txt
fi
}
workon() {
local project_name="$1"
local projects_file="$HOME/.projects"
local project_dir
# Check for projects config file
if [[ ! -f "$projects_file" ]]; then
echo "Error: $projects_file not found" >&2
return 1
fi
# Get the project directory for the given project name
project_dir=$(grep -E "^$project_name\s*=" "$projects_file" | sed 's/^[^=]*=\s*//')
# Ensure a project directory was found
if [[ -z "$project_dir" ]]; then
echo "Error: Project '$project_name' not found in $projects_file" >&2
return 1
fi
# Ensure the project directory exists
if [[ ! -d "$project_dir" ]]; then
echo "Error: Directory $project_dir does not exist" >&2
return 1
fi
# Change directories
cd "$project_dir"
}
rmvenv() {
# Remove a virtual environment
local venv_name
local projects_file="$HOME/.projects"
local dir_name=$(basename "$PWD")
# If there are no arguments or the last argument starts with a dash, use dir_name
if [ $# -eq 0 ] || [[ "${!#}" == -* ]]; then
venv_name="$dir_name"
else
venv_name="${!#}"
set -- "${@:1:$#-1}"
workon $venv_name
fi
# Check if .envrc already exists
if [ -f .envrc ]; then
echo "Removing .envrc"
rm .envrc
fi
if [ -d .venv ]; then
echo "Removing .venv"
rm -rf .venv
fi
if [ -d .direnv ]; then
echo "Removing .direnv"
rm -rf .direnv
fi
if grep -Fq ${venv_name} ${projects_file}; then
echo "Removing ${venv_name} from ${projects_file}"
sed -i "/^${venv_name}/d" ${projects_file}
fi
}