<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thomas Bury</title>
    <description>The latest articles on DEV Community by Thomas Bury (@thomas_bury_b1a50c1156cbf).</description>
    <link>https://dev.to/thomas_bury_b1a50c1156cbf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1717423%2F1fcb1dca-ae54-48e6-97f5-0ffaa19dabd1.jpg</url>
      <title>DEV Community: Thomas Bury</title>
      <link>https://dev.to/thomas_bury_b1a50c1156cbf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thomas_bury_b1a50c1156cbf"/>
    <language>en</language>
    <item>
      <title>Mastering Python Project Management with uv: Part 5 — advanced CI/CD Nox and uv.cli</title>
      <dc:creator>Thomas Bury</dc:creator>
      <pubDate>Sat, 12 Apr 2025 19:22:34 +0000</pubDate>
      <link>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-5-advanced-cicd-nox-and-uvcli-47a5</link>
      <guid>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-5-advanced-cicd-nox-and-uvcli-47a5</guid>
      <description>&lt;p&gt;Let's dive in advanced Python automation with &lt;code&gt;nox&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, and GitHub Actions — Part 5 of the Terminus Series&lt;/p&gt;

&lt;p&gt;Building on &lt;a href="https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-4-cicd-docker-385e"&gt;Part 4&lt;/a&gt;, where we introduced fast, modern MLOps pipelines using &lt;code&gt;uv&lt;/code&gt;, Docker, and CI/CD, this fifth installment focuses on automating your developer workflow using Nox — and why in 2025 it still reigns as one of the most flexible, transparent tools for Python-based projects.&lt;/p&gt;

&lt;p&gt;This article walks through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ What exactly Nox is and why it's still relevant in 2025&lt;/li&gt;
&lt;li&gt;🧰 How we configure our noxfile in &lt;a href="https://github.com/ThomasBury/terminus" rel="noopener noreferrer"&gt;Terminus&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 How GitHub Actions can dynamically discover and run your nox sessions&lt;/li&gt;
&lt;li&gt;⚖️ A brief but robust comparison of &lt;code&gt;nox&lt;/code&gt;, &lt;code&gt;Poe the Poet&lt;/code&gt;, and &lt;code&gt;uv&lt;/code&gt; CLI&lt;/li&gt;
&lt;li&gt;🏗️ Why Nox, despite the rise of CLI runners, still plays a crucial orchestration role&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Absolutely! Here's a more comprehensive and pedantic rewrite of the section &lt;strong&gt;"Going Deeper with uv CLI Tools"&lt;/strong&gt;, structured to clearly guide the reader, explain motivations and use cases, and gracefully transition into the next section on &lt;code&gt;nox&lt;/code&gt;:&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Deeper with &lt;code&gt;uv&lt;/code&gt; CLI Tools — Beyond &lt;code&gt;uv sync&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Up to this point, we've mostly used &lt;code&gt;uv&lt;/code&gt; as a &lt;strong&gt;fast, lockfile-aware dependency resolver&lt;/strong&gt; — replacing &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;pip-tools&lt;/code&gt;, or &lt;code&gt;poetry&lt;/code&gt; for installing packages via &lt;code&gt;uv sync&lt;/code&gt;. But that’s only scratching the surface.&lt;/p&gt;

&lt;p&gt;In practice, &lt;code&gt;uv&lt;/code&gt; is becoming a holistic &lt;strong&gt;developer toolchain manager&lt;/strong&gt; — and in projects like &lt;strong&gt;Terminus&lt;/strong&gt;, it eliminates the need for &lt;code&gt;make&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, or even task runners like &lt;code&gt;poethepoet&lt;/code&gt; in many cases.&lt;/p&gt;

&lt;p&gt;Let’s explore the full potential of &lt;code&gt;uv&lt;/code&gt; as a modern CLI toolchain — both for local development and CI/CD pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;uv tool install&lt;/code&gt; and &lt;code&gt;uv tool run&lt;/code&gt;: Global CLI without Virtualenv Pain
&lt;/h3&gt;

&lt;p&gt;While &lt;code&gt;uv sync&lt;/code&gt; installs project-scoped dependencies into virtual environments, &lt;strong&gt;&lt;code&gt;uv tool&lt;/code&gt; manages CLI tools globally&lt;/strong&gt;, with user isolation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;nox
uv tool run nox &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installs &lt;code&gt;nox&lt;/code&gt; (or any CLI tool) into a dedicated global environment at &lt;code&gt;~/.local/share/uv/tools&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Keeps these tools separate from your project env or system Python&lt;/li&gt;
&lt;li&gt;Ensures reproducibility across developer machines and CI — version-pinned via &lt;code&gt;uv.lock&lt;/code&gt; if needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoids &lt;code&gt;venv&lt;/code&gt; activation&lt;/strong&gt;, making developer onboarding zero-friction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In CI&lt;/strong&gt; (as in Terminus), this means we don't need to activate a venv, use &lt;code&gt;pipx&lt;/code&gt;, or worry about system-wide &lt;code&gt;pip&lt;/code&gt; issues. We simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;nox
uv tool run nox &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Caching CLI Tools Between CI Runs
&lt;/h3&gt;

&lt;p&gt;One performance advantage of &lt;code&gt;uv tool&lt;/code&gt; is that CLI tools are &lt;strong&gt;cached globally&lt;/strong&gt; in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.local/share/uv/tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By caching this directory in GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.local/share/uv/tools&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv-tools-${{ runner.os }}-${{ hashFiles('pyproject.toml', '**/uv.lock') }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid repeated downloads or rebuilds of CLI tools (e.g., &lt;code&gt;nox&lt;/code&gt;, &lt;code&gt;ruff&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Keep your build minimal and declarative — no surprise installs during jobs&lt;/li&gt;
&lt;li&gt;Ensure clarity between “project dependencies” (&lt;code&gt;uv sync&lt;/code&gt;) and “development tools” (&lt;code&gt;uv tool&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Defining CLI Commands with &lt;code&gt;[tool.uv.cli]&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Just like &lt;code&gt;poethepoet&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt; lets you define custom developer commands directly in &lt;code&gt;pyproject.toml&lt;/code&gt; — no extra dependencies needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv.cli]&lt;/span&gt;
&lt;span class="py"&gt;lint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ruff check src"&lt;/span&gt;
&lt;span class="py"&gt;format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ruff format src"&lt;/span&gt;
&lt;span class="py"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pytest tests/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run them with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run lint
uv run &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the ergonomics of a task runner (like &lt;code&gt;make&lt;/code&gt; or &lt;code&gt;poe&lt;/code&gt;) &lt;strong&gt;without adding a new config language or runtime dependency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can think of it as &lt;strong&gt;task aliases bound to your dependency context&lt;/strong&gt;, all managed by &lt;code&gt;uv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Recommendations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;&lt;code&gt;uv run&lt;/code&gt; or &lt;code&gt;[tool.uv.cli]&lt;/code&gt;&lt;/strong&gt; for simple local dev flows: &lt;code&gt;lint&lt;/code&gt;, &lt;code&gt;serve&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;&lt;code&gt;poethepoet&lt;/code&gt;&lt;/strong&gt; if you need cross-platform &lt;code&gt;make&lt;/code&gt;-like chaining with config in &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;&lt;code&gt;nox&lt;/code&gt;&lt;/strong&gt; if you need session orchestration, multi-version testing, and dynamic CI compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now that we understand how &lt;code&gt;uv&lt;/code&gt; and &lt;code&gt;uv tool&lt;/code&gt; fit into modern Python projects, let’s look at how we use &lt;code&gt;nox&lt;/code&gt; in Terminus to automate linting, testing, and Docker builds — in both local and CI pipelines.&lt;/p&gt;

&lt;p&gt;Let’s dive into &lt;code&gt;nox&lt;/code&gt; and dynamic CI automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is &lt;code&gt;nox&lt;/code&gt;, and Why Use It?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nox.thea.codes/en/stable/index.html" rel="noopener noreferrer"&gt;Nox&lt;/a&gt; is a Python automation tool designed to run sessions — isolated environments where you install dependencies and run scripts.&lt;/p&gt;

&lt;p&gt;Unlike &lt;code&gt;make&lt;/code&gt;, &lt;code&gt;poe&lt;/code&gt;, or raw &lt;code&gt;shell&lt;/code&gt; scripts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each session is run in a fresh virtual environment (by default)&lt;/li&gt;
&lt;li&gt;You can target multiple Python versions per session&lt;/li&gt;
&lt;li&gt;Dependencies can be scoped using a tool like &lt;code&gt;uv&lt;/code&gt;, &lt;code&gt;pip&lt;/code&gt;, or &lt;code&gt;poetry&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It’s &lt;strong&gt;Python-native&lt;/strong&gt;: no new DSL, write Python functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Terminus, we use &lt;code&gt;nox&lt;/code&gt; as the automation layer for tasks like linting and testing and can be extended to build the docs, clean up etc.&lt;/p&gt;

&lt;p&gt;This makes Nox ideal for &lt;strong&gt;CI workflows&lt;/strong&gt; — especially when the definition of what to run changes over time (via dynamic session discovery).&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminus noxfile.py — Dissection
&lt;/h2&gt;

&lt;p&gt;Let’s look at how Terminus’ &lt;code&gt;noxfile.py&lt;/code&gt; enforces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single source of truth for Python version (extracted from &lt;code&gt;pyproject.toml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Group-based dependency installation via &lt;code&gt;uv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dev-friendly modular automation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📄 &lt;a href="https://github.com/ThomasBury/terminus/blob/main/noxfile.py" rel="noopener noreferrer"&gt;terminus/noxfile.py&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Parses Python version from pyproject.toml
# Sets uv as the backend
# Defines two nox sessions: lint
&lt;/span&gt;
&lt;span class="nd"&gt;@nox.session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PYTHON_VERSION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run Ruff linter and formatter checks.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LINT_GROUP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--python=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UV_PROJECT_ENVIRONMENT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;virtualenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ruff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CODE_DIR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ruff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CODE_DIR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses &lt;code&gt;uv&lt;/code&gt; as venv backend for &lt;em&gt;speed&lt;/em&gt; and lockfile support.&lt;/li&gt;
&lt;li&gt;Reads the required Python version directly from &lt;code&gt;pyproject.toml&lt;/code&gt; for full alignment across local + CI workflows.&lt;/li&gt;
&lt;li&gt;Dependency isolation: installs only the &lt;code&gt;lint&lt;/code&gt; group, not the full app stack.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Dynamic Matrix CI with GitHub Actions + Nox
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding jobs, Terminus uses a modern matrix approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Determine Python version and available nox sessions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate-matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sessions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.set-matrix.outputs.sessions }}&lt;/span&gt;
      &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.get-python-version.outputs.version }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Reads the Python version from &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extracts nox sessions via &lt;code&gt;nox --json -l&lt;/code&gt; and filters with &lt;code&gt;jq&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All this happens before the test matrix runs, so any added sessions automatically trigger in CI without manual YAML changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Run each session in parallel
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;run-nox-sessions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ fromJson(needs.generate-matrix.outputs.sessions) }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nox -s "${{ matrix.session }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each session is isolated, fast, and reproducible — and uses &lt;code&gt;uv&lt;/code&gt; as the environment and dependency manager.&lt;/p&gt;




&lt;h2&gt;
  
  
  Side-by-side Comparison: &lt;code&gt;nox&lt;/code&gt; vs &lt;code&gt;poe&lt;/code&gt; vs &lt;code&gt;uv cli&lt;/code&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;code&gt;nox&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;poethepoet&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;uv&lt;/code&gt; CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python-native&lt;/td&gt;
&lt;td&gt;Custom &lt;code&gt;pyproject&lt;/code&gt; syntax&lt;/td&gt;
&lt;td&gt;Built-in to &lt;code&gt;uv&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual Envs&lt;/td&gt;
&lt;td&gt;Yes (isolated per session)&lt;/td&gt;
&lt;td&gt;No (uses current env)&lt;/td&gt;
&lt;td&gt;Optional via &lt;code&gt;uv venv&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI Task Syntax&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nox -s lint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;poe lint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv run ruff check src&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI Friendly&lt;/td&gt;
&lt;td&gt;✅ Matrix + Python version&lt;/td&gt;
&lt;td&gt;⚠️ Mostly local convenience&lt;/td&gt;
&lt;td&gt;✅ (if you know the commands)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session Scoping&lt;/td&gt;
&lt;td&gt;Per-group / per-version&lt;/td&gt;
&lt;td&gt;Globally defined&lt;/td&gt;
&lt;td&gt;Command-line scoped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Matrix Discovery&lt;/td&gt;
&lt;td&gt;✅ (&lt;code&gt;--json&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modern Lock Support&lt;/td&gt;
&lt;td&gt;✅ via &lt;code&gt;uv sync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ via &lt;code&gt;uv&lt;/code&gt; if integrated&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  So when to use each?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;poe&lt;/code&gt; or &lt;code&gt;uv cli&lt;/code&gt; for small, script-oriented workflows and fast local CLI access.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;nox&lt;/code&gt; when you:

&lt;ul&gt;
&lt;li&gt;Need multiple Python versions&lt;/li&gt;
&lt;li&gt;Want true isolation per task&lt;/li&gt;
&lt;li&gt;Want to share automation across dev + CI reliably&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Full Automation: From Code to Container
&lt;/h2&gt;

&lt;p&gt;Once lint and test sessions pass, our CI continues to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the image (multi-stage Docker build)&lt;/li&gt;
&lt;li&gt;Authenticate to Docker Hub&lt;/li&gt;
&lt;li&gt;Push image tagged by short SHA + &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Defined here: &lt;a href="https://github.com/ThomasBury/terminus/blob/main/.github/workflows/ci.yml" rel="noopener noreferrer"&gt;.github/workflows/ci.yml&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;While new tools like &lt;code&gt;uv&lt;/code&gt; and &lt;code&gt;poethepoet&lt;/code&gt; offer impressive speed and simplicity, &lt;strong&gt;Nox remains an unparalleled orchestration layer&lt;/strong&gt; in 2025 for teams needing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python version agility&lt;/li&gt;
&lt;li&gt;Lockfile-aware sessions&lt;/li&gt;
&lt;li&gt;Matrix-based CI pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Terminus, combining Nox with &lt;code&gt;uv&lt;/code&gt;, GitHub Actions, and &lt;code&gt;Docker&lt;/code&gt; gives us a maintainable and fully automated pipeline — fast, reproducible, and clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codebase
&lt;/h3&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/ThomasBury/terminus" rel="noopener noreferrer"&gt;View Terminus on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;noxfile.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Full GitHub Actions workflow (&lt;code&gt;ci.yml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Example &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;src/&lt;/code&gt;, and &lt;code&gt;tests/&lt;/code&gt; (TODO)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mlops</category>
      <category>cicd</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>Mastering Python Project Management with uv: Part 4 — CI/CD &amp; Docker</title>
      <dc:creator>Thomas Bury</dc:creator>
      <pubDate>Thu, 10 Apr 2025 09:42:07 +0000</pubDate>
      <link>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-4-cicd-docker-385e</link>
      <guid>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-4-cicd-docker-385e</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://bury-thomas.medium.com/mastering-python-project-management-with-uv-part-4-ci-cd-docker-ed4128fdd0c1" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Terminus Edition&lt;/strong&gt; — Modern CI/CD and Deployment Workflows for LLM-Powered Apps&lt;/p&gt;

&lt;h2&gt;
  
  
  Context: LLM-Powered Apps Meet Modern Tooling
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://bury-thomas.medium.com/mastering-python-project-management-with-uv-part-3-mlops-4906965f45a2" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt; of the series, we explored how &lt;code&gt;uv&lt;/code&gt; can simplify dependency management and improve MLOps for packaged Python applications. Now it’s time to take your project to production.&lt;/p&gt;

&lt;p&gt;This part focuses on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Dockerizing your app the modern way&lt;/li&gt;
&lt;li&gt;  Using &lt;code&gt;docker-compose&lt;/code&gt; for local development&lt;/li&gt;
&lt;li&gt;  Automating CI/CD with GitHub Actions&lt;/li&gt;
&lt;li&gt;  Linting with &lt;code&gt;ruff&lt;/code&gt; and running tests with &lt;code&gt;uv run&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll use &lt;a href="https://github.com/ThomasBury/terminus" rel="noopener noreferrer"&gt;&lt;strong&gt;Terminus&lt;/strong&gt;&lt;/a&gt; as the reference project—a FastAPI app that defines and validates user-defined topic-related terms using LLMs (Instructor + LiteLLM), Wikipedia, and Pydantic guardrails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-Ready Docker Builds with &lt;code&gt;uv&lt;/code&gt;: The Two-Stage Advantage
&lt;/h2&gt;

&lt;p&gt;When containerizing a Python application, it’s tempting to throw everything into a single &lt;code&gt;Dockerfile&lt;/code&gt;, install some packages, run your code, and call it a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not if you care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  ⚡️ &lt;em&gt;Build speed&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  🐍 &lt;em&gt;Clean dependency separation&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  📦 &lt;em&gt;Image size&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  🔒 &lt;em&gt;Security&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the &lt;strong&gt;two-stage Docker build&lt;/strong&gt; pattern shines — and paired with &lt;code&gt;uv&lt;/code&gt;, the blazing-fast modern package manager, elevates Python Docker workflows to a new level of performance and simplicity.&lt;/p&gt;

&lt;p&gt;Let’s dissect Terminus’ Dockerfile and understand what makes it tick.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A700%2F0%2AkDffBFN5xTggk-eA" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A700%2F0%2AkDffBFN5xTggk-eA" alt="uv two-stage docker" width="700" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: Build Stage (a.k.a. “The Builder”)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.13-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We start from a slim Python base — minimal but powerful. Here’s where the magic happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Install&lt;/strong&gt; &lt;code&gt;uv&lt;/code&gt;:
&lt;code&gt;uv&lt;/code&gt; is installed via &lt;code&gt;pip&lt;/code&gt;, giving us a &amp;gt;10x faster resolver than &lt;code&gt;pip&lt;/code&gt; or &lt;code&gt;Poetry&lt;/code&gt;.
It's designed for speed, reliability, and modern packaging workflows.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Install Build Dependencies&lt;/strong&gt;:
We temporarily install system packages like &lt;code&gt;build-essential&lt;/code&gt; and &lt;code&gt;curl&lt;/code&gt; — necessary for compiling Python wheels (think &lt;code&gt;psycopg2&lt;/code&gt;, &lt;code&gt;lxml&lt;/code&gt;, etc.).
These are later &lt;em&gt;purged&lt;/em&gt; to avoid bloating the final image.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Leverage Layer Caching&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock README.md ./&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only metadata is copied first. Why? Because Docker caches layers. If your dependencies haven’t changed, Docker will &lt;strong&gt;reuse the layer&lt;/strong&gt;, dramatically speeding up future builds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Install Dependencies&lt;/strong&gt;:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;RUN uv pip install --system . ...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Dependencies are installed directly into the &lt;em&gt;system Python&lt;/em&gt;, not a virtual environment. This avoids virtualenv overhead, speeds up startup times, and is more memory efficient in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Copy Application Code&lt;/strong&gt;: Source code is copied last so that frequent edits don’t bust the dependency cache. Clean separation of concerns!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stage 2: Runtime Stage (a.k.a. “the user”)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.13-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stage is purposefully lean and minimal. It contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Only the runtime Python environment and dependencies&lt;/li&gt;
&lt;li&gt;  Your application source code&lt;/li&gt;
&lt;li&gt;  No compilers, no wheel caches, no source &lt;code&gt;.pyc&lt;/code&gt;, no tools you don’t need in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=base /usr/local /usr/localCOPY --from=base /app /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This includes installed packages and the application itself — and nothing more.&lt;/p&gt;

&lt;p&gt;We also define:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /appENV PYTHONPATH=/app/src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures Python modules resolve properly when launched via FastAPI, CLI, or background tasks.&lt;/p&gt;

&lt;p&gt;Result: A final image that is &lt;strong&gt;much smaller (sometimes up to 80%)&lt;/strong&gt;, loads faster, and is easier to secure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Smaller Images:&lt;/strong&gt; no dev tools, no compilers, no cache = smaller size = faster pull times &amp;amp; fewer cloud resources.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Faster CI/CD:&lt;/strong&gt; metadata-first copying + &lt;code&gt;uv&lt;/code&gt;'s resolver = lightning-fast builds and rebuilds.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Cleaner Production:&lt;/strong&gt; no stray &lt;code&gt;.pyc&lt;/code&gt;, no untracked dependencies, just the essentials.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Better Security:&lt;/strong&gt; smaller attack surface; no dev tools left behind.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Reproducibility:&lt;/strong&gt; &lt;code&gt;uv.lock&lt;/code&gt; ensures the same dependency versions across machines and builds.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Speed:&lt;/strong&gt; &lt;code&gt;uv&lt;/code&gt; + &lt;code&gt;ruff&lt;/code&gt; make linting and formatting lightning fast — perfect for CI/CD (see GitHub Actions below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tips for Getting It Right:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Always copy your &lt;strong&gt;lock file&lt;/strong&gt; and &lt;code&gt;pyproject.toml&lt;/code&gt; early to benefit from Docker layer caching.&lt;/li&gt;
&lt;li&gt;  Install your dependencies using &lt;code&gt;uv pip install --system .&lt;/code&gt; — no need for &lt;code&gt;venv&lt;/code&gt; or Poetry hacks.&lt;/li&gt;
&lt;li&gt;  Strip your build stage with &lt;code&gt;apt-get purge&lt;/code&gt; and &lt;code&gt;rm -rf&lt;/code&gt; to keep the final image lean.&lt;/li&gt;
&lt;li&gt;  Let Docker Compose or your runtime environment define the &lt;code&gt;CMD&lt;/code&gt;, so the base image stays flexible and composable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can see this pattern fully implemented in Terminus’ &lt;a href="https://github.com/ThomasBury/terminus/blob/main/Dockerfile" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; along with the corresponding GitHub Actions workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  docker-compose with &lt;code&gt;uv&lt;/code&gt;: Reproducibility meets velocity
&lt;/h2&gt;

&lt;p&gt;Now that we’ve built a clean, layered Docker image using a two-stage &lt;code&gt;Dockerfile&lt;/code&gt;Let’s orchestrate it for local development using &lt;code&gt;docker-compose&lt;/code&gt;. This is where &lt;code&gt;uv&lt;/code&gt; starts to shine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker-compose&lt;/code&gt; lets you declaratively define how your app is built, configured, and run — across environments. Combined with &lt;code&gt;uv&lt;/code&gt;, it gives us a &lt;strong&gt;fast, reproducible, and developer-friendly&lt;/strong&gt; setup.&lt;/p&gt;

&lt;p&gt;Let’s walk through the key pieces and highlight why &lt;code&gt;uv&lt;/code&gt; changes the game:&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast Iteration: Live Reloading with Mounted Volumes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;- ./src:/app/src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mounts your local &lt;code&gt;src/&lt;/code&gt; directory directly inside the container. Any code changes you make on your machine are reflected immediately in the container — no rebuilds, no restarts (not meant for production).&lt;/p&gt;

&lt;p&gt;Coupled with Uvicorn’s &lt;code&gt;--reload&lt;/code&gt; flag, this enables &lt;strong&gt;hot-reloading&lt;/strong&gt; of your FastAPI app during development — a productivity boost that feels native, even inside Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean, Locked Environments with &lt;code&gt;uv&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of relying on &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;requirements.txt&lt;/code&gt;, we use &lt;code&gt;uv&lt;/code&gt; inside the container to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Install dependencies &lt;strong&gt;from&lt;/strong&gt; &lt;code&gt;pyproject.toml&lt;/code&gt;, keeping the setup declarative and modern.&lt;/li&gt;
&lt;li&gt;  Enforce &lt;strong&gt;lockfile-based reproducibility&lt;/strong&gt; using &lt;code&gt;uv.lock&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Run tools like &lt;code&gt;ruff&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt;, or &lt;code&gt;uvicorn&lt;/code&gt; &lt;strong&gt;without globally installing them&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to &lt;code&gt;uv&lt;/code&gt;’s binary-first architecture, dependency resolution is blazingly fast, and the dev container is ready to go in seconds — no virtualenv juggling needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment Isolation Done Right
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PYTHONPATH=/app/src&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=sqlite:///data/terminus.db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storing environment-specific settings (like API keys, model providers, and database paths) in a &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; respects these variables at runtime, and we explicitly set &lt;code&gt;PYTHONPATH&lt;/code&gt; to ensure imports like &lt;code&gt;from terminus.models import ...&lt;/code&gt; behave identically inside and outside Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health Checks for Early Failure Detection
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/docs"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tiny but powerful snippet tells Docker: “My app is alive if &lt;code&gt;/docs&lt;/code&gt; responds.” If something breaks — like an LLM outage or a misconfigured &lt;code&gt;.env&lt;/code&gt; — you’ll see it in &lt;code&gt;docker ps&lt;/code&gt; right away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-m&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uvicorn&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;src.terminus.app:app&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--host&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--port&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we use &lt;code&gt;uvicorn&lt;/code&gt; to launch the app in development mode with live reload. By using &lt;code&gt;python -m uvicorn&lt;/code&gt;, we make this command shell-agnostic and easier to port to other entrypoints (e.g., for Gunicorn in production).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;uv&lt;/code&gt; Makes This Setup Better
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7terugm36xzl30thxsgw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7terugm36xzl30thxsgw.png" alt="comparison table" width="671" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In short, &lt;code&gt;uv&lt;/code&gt; gives you modern dependency management that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Declarative&lt;/strong&gt; (single source of truth in &lt;code&gt;pyproject.toml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Reproducible&lt;/strong&gt; (locked versions across machines)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fast&lt;/strong&gt; (Rust-powered resolver is wicked quick)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Tool-friendly&lt;/strong&gt; (no pipx, no venv gymnastics)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you care about &lt;strong&gt;speed&lt;/strong&gt;, &lt;strong&gt;reproducibility&lt;/strong&gt;, and &lt;strong&gt;a frictionless dev environment&lt;/strong&gt;, this pairing of &lt;code&gt;uv&lt;/code&gt; with &lt;code&gt;docker-compose&lt;/code&gt; is hard to beat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0cbytacyj9o6yy28pexa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0cbytacyj9o6yy28pexa.png" alt="uv docker-compose" width="700" height="1492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions + &lt;code&gt;uv&lt;/code&gt;: Clean CI/CD Pipelines
&lt;/h2&gt;

&lt;p&gt;Your app is structured. Your Docker image is lean. Your development flow is fast.&lt;/p&gt;

&lt;p&gt;Now it’s time to level up your &lt;strong&gt;continuous integration and delivery&lt;/strong&gt; (CI/CD) with a GitHub Actions pipeline that’s just as modern as &lt;code&gt;uv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When done right, your CI should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Lint for correctness and style&lt;/li&gt;
&lt;li&gt;  Run tests to catch regressions (shame on me, Terminus has no test suite yet)&lt;/li&gt;
&lt;li&gt;  Build and publish a Docker image (public or private Hub)&lt;/li&gt;
&lt;li&gt;  Use the same dependency versions as local development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s break down what we’re doing and why &lt;code&gt;uv&lt;/code&gt; makes it not only possible but elegant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx02ilmeqgeux8prq5iz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx02ilmeqgeux8prq5iz.png" alt="uv cicd github actions flow" width="700" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lint and Validate (the fast way)
&lt;/h2&gt;

&lt;p&gt;You don’t want broken code, and you want to know &lt;em&gt;why&lt;/em&gt; it’s broken as early as possible.&lt;/p&gt;

&lt;p&gt;That’s where &lt;a href="https://docs.astral.sh/ruff/" rel="noopener noreferrer"&gt;Ruff&lt;/a&gt; comes in — an ultra-fast Python linter and formatter that’s &lt;strong&gt;written in Rust&lt;/strong&gt; and integrates perfectly with &lt;code&gt;uv&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;✨ Lint with Ruff&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;uv run ruff check src&lt;/span&gt;
    &lt;span class="s"&gt;uv run ruff format --check src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs &lt;strong&gt;two critical checks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;ruff check&lt;/code&gt;: finds bugs, code smells, and style violations.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;ruff format --check&lt;/code&gt;: enforces formatting, CI-friendly (it fails if formatting isn’t followed).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why this is great:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Runs in seconds&lt;/li&gt;
&lt;li&gt;  Uses the exact version of Ruff locked in &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Zero global installs — all inside the &lt;code&gt;uv&lt;/code&gt; runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test, Reproducibly
&lt;/h2&gt;

&lt;p&gt;Assuming you’ve defined your test suite (e.g., with &lt;code&gt;pytest&lt;/code&gt; or &lt;code&gt;httpx&lt;/code&gt; for API tests), running it is a breeze:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;✅ Run tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run pytest tests/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why we love this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Tests run in a clean, ephemeral environment (not your laptop!)&lt;/li&gt;
&lt;li&gt;  Always use the same versions of &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;httpx&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;  Optional: You can mock LLM calls or run against a test &lt;code&gt;.env&lt;/code&gt; file for safe validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;uv sync&lt;/code&gt; secret sauce
&lt;/h2&gt;

&lt;p&gt;Before linting or testing, we install all dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;📦 Install dependencies&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv sync --dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Resolves and installs all &lt;strong&gt;production&lt;/strong&gt; + &lt;strong&gt;development&lt;/strong&gt; dependencies (Ruff, Pytest, etc.)&lt;/li&gt;
&lt;li&gt;  Uses the exact versions from your &lt;code&gt;uv.lock&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Skips virtualenvs and &lt;code&gt;pip install .&lt;/code&gt; nonsense&lt;/li&gt;
&lt;li&gt;  Guarantees your CI runs &lt;strong&gt;exactly&lt;/strong&gt; like your local dev container or machine&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build and Push a Docker Image
&lt;/h2&gt;

&lt;p&gt;In a separate job (&lt;code&gt;build-and-push-docker&lt;/code&gt;), we only trigger the Docker build &lt;em&gt;after&lt;/em&gt; tests pass — because shipping broken containers helps no one.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;docker/metadata-action&lt;/code&gt; to auto-generate tags like &lt;code&gt;latest&lt;/code&gt; and a short Git SHA, and push the image to Docker Hub using your GitHub repository secrets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKERHUB_USERNAME }}/terminus&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;type=sha,format=short&lt;/span&gt;
  &lt;span class="s"&gt;type=raw,value=latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want reproducibility and traceability? Use the SHA-based tag to pin an image to a commit forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;uv&lt;/code&gt; Makes GitHub Actions Better
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzo44fwnhexmjg8xvjo74.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzo44fwnhexmjg8xvjo74.png" alt="comparison table" width="646" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;uv&lt;/code&gt;, your GitHub Actions become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Faster (no dependency juggling)&lt;/li&gt;
&lt;li&gt;  Reproducible (locked versions, always)&lt;/li&gt;
&lt;li&gt;  Cleaner (no extra YAML for managing Python tools)&lt;/li&gt;
&lt;li&gt;  More secure (no extra tools needed outside the &lt;code&gt;uv&lt;/code&gt; runtime)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Publishing to Docker Hub — the Right Way
&lt;/h2&gt;

&lt;p&gt;Once our application is linted, tested, and deemed production-ready, the final piece is building and pushing a Docker image to a registry. In this case: Docker Hub.&lt;/p&gt;

&lt;p&gt;But we’re not just pushing any image. We’re pushing a &lt;strong&gt;reproducible&lt;/strong&gt;, &lt;strong&gt;lightweight&lt;/strong&gt;, and &lt;strong&gt;well-tagged&lt;/strong&gt; artifact — built with &lt;code&gt;uv&lt;/code&gt;, orchestrated by GitHub Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authenticate Securely (no credentials in sight)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of hardcoding usernames or passwords (a security anti-pattern), we use GitHub repository &lt;strong&gt;secrets&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKERHUB_USERNAME }}&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKERHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These should be added in your repo under: Settings → Secrets and variables → Actions&lt;/p&gt;

&lt;p&gt;You’ll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;DOCKERHUB_USERNAME&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;DOCKERHUB_TOKEN&lt;/code&gt; (Docker Hub → Account Settings → Security → New Access Token)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These secrets never appear in logs&lt;br&gt;&lt;br&gt;
They auto-expire if you revoke them&lt;br&gt;&lt;br&gt;
They protect your pipeline from leakage&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-Generate Metadata and Tags&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of manually tagging your Docker images (error-prone and easy to forget), we use the &lt;code&gt;docker/metadata-action&lt;/code&gt; to auto-tag based on Git info:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/metadata-action@v5&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKERHUB_USERNAME }}/terminus&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;type=sha,format=short        # Tag image with commit SHA (e.g., terminus:abc1234)&lt;/span&gt;
      &lt;span class="s"&gt;type=raw,value=latest        # Also push as "latest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Traceability&lt;/strong&gt;: Every pushed image is linked to a commit.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Convenience&lt;/strong&gt;: &lt;code&gt;:latest&lt;/code&gt; is available for local dev and quick pull.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Automation&lt;/strong&gt;: No human errors in manual tagging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Build Once, reuse always&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thanks to Docker BuildKit and layer caching via GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
&lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Faster builds (because unchanged layers are reused)&lt;/li&gt;
&lt;li&gt;  Lower CI costs (fewer image pulls/uploads)&lt;/li&gt;
&lt;li&gt;  Deterministic environments (same code = same image)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The build is based on your &lt;strong&gt;two-stage Dockerfile&lt;/strong&gt; (explained above), which ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Minimal attack surface (no dev tools in final image)&lt;/li&gt;
&lt;li&gt;  Fast cold starts (no bloat, clean dependencies)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;uv&lt;/code&gt; is used &lt;em&gt;inside&lt;/em&gt; the image too — so the containerized app benefits from the same ultra-fast resolution and install.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Push to Docker Hub&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finally, the &lt;code&gt;docker/build-push-action&lt;/code&gt; does the actual build &amp;amp; publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.tags }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once this step is completed, your image is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Built with locked &lt;code&gt;uv&lt;/code&gt; dependencies&lt;/li&gt;
&lt;li&gt;  Tagged with Git commit + &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Published to Docker Hub, ready to be &lt;code&gt;docker pull&lt;/code&gt;ed anywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This step finalizes the loop of &lt;strong&gt;build → validate → ship&lt;/strong&gt; — with every part driven by &lt;code&gt;uv&lt;/code&gt;'s speed and consistency and GitHub Actions' automation.&lt;/p&gt;

&lt;p&gt;Let’s wrap up this build-and-deploy journey by zooming out and synthesizing everything we’ve accomplished.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We’ve Built — A Fast, Clean, and Reproducible CI/CD Pipeline with &lt;code&gt;uv&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s step back and admire the simplicity and power of the DevOps pipeline we’ve just assembled. What started as a FastAPI app &lt;code&gt;Terminus&lt;/code&gt; is now a &lt;strong&gt;fully containerized, continuously validated, and registry-published service&lt;/strong&gt; — all thanks to a few strategic tools and good practices.&lt;/p&gt;

&lt;p&gt;Combining all the steps in &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terminus CI/CD&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# trigger on tags for versioned releases&lt;/span&gt;
    &lt;span class="c1"&gt;# tags: ['v*.*.*']&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;PYTHON_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.13'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint-and-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint &amp;amp; Test&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;⬇️ Checkout Code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🐍 Setup Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.PYTHON_VERSION }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;📦 Install uv&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v5&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🧠 Restore uv cache&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cache-uv&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.cache/uv&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv-${{ runner.os }}-${{ hashFiles('pyproject.toml', 'uv.lock') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;uv-${{ runner.os }}-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;📦 Install dependencies (lint)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv sync --group lint&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;✨ Lint with Ruff&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;uv run ruff check src&lt;/span&gt;
          &lt;span class="s"&gt;uv run ruff format --check src&lt;/span&gt;
    &lt;span class="c1"&gt;# Shame, no tests yet&lt;/span&gt;
    &lt;span class="c1"&gt;#   - name: ✅ Run tests&lt;/span&gt;
    &lt;span class="c1"&gt;#     run: uv run pytest tests/&lt;/span&gt;
    &lt;span class="c1"&gt;#     continue-on-error: true  # Make strict if critical&lt;/span&gt;

  &lt;span class="na"&gt;build-and-push-docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Push Docker Image&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push' &amp;amp;&amp;amp; github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint-and-test&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;⬇️ Checkout Code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🐳 Set up Docker Buildx&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🏷️ Docker Metadata&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;meta&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/metadata-action@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKERHUB_USERNAME }}/terminus&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;type=sha,format=short&lt;/span&gt;
            &lt;span class="s"&gt;type=raw,value=latest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🔑 Log in to Docker Hub&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKERHUB_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKERHUB_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🏗️ Build and Push Docker Image&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.tags }}&lt;/span&gt;
          &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.labels }}&lt;/span&gt;
          &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
          &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what makes this pipeline shine:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;uv&lt;/code&gt;: the dependency backbone
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Lightning-fast resolution and install&lt;/strong&gt; compared to pip + venv.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lockfile (&lt;code&gt;uv.lock&lt;/code&gt;)&lt;/strong&gt; ensures reproducibility across environments (local, CI, Docker).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Single source of truth&lt;/strong&gt; for runtime &lt;em&gt;and&lt;/em&gt; dev dependencies via &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you’re syncing a new dev machine or building production containers, you run the same thing: &lt;code&gt;uv sync&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker, the fast way
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Two-stage build&lt;/strong&gt; keeps dev tools and build dependencies &lt;em&gt;out&lt;/em&gt; of the final image.&lt;/li&gt;
&lt;li&gt;  Uses &lt;code&gt;uv&lt;/code&gt; to &lt;strong&gt;install directly into system Python&lt;/strong&gt;, skipping venv overhead.&lt;/li&gt;
&lt;li&gt;  The final image is minimal, fast, and portable — optimized for cloud environments or edge deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A clean image = smaller surface = faster startup = happier users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions, but smarter&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Separate stages for linting, testing, and deployment&lt;/strong&gt; improve clarity and feedback loops.&lt;/li&gt;
&lt;li&gt;  Linting with &lt;code&gt;ruff&lt;/code&gt;, testing with &lt;code&gt;pytest&lt;/code&gt;, and building with &lt;code&gt;docker&lt;/code&gt; — all run via &lt;code&gt;uv&lt;/code&gt;, ensuring dev/prod parity.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Docker Hub publishing&lt;/strong&gt; happens &lt;em&gt;only when&lt;/em&gt; tests pass and &lt;em&gt;only on&lt;/em&gt; the &lt;code&gt;main&lt;/code&gt; branch — the golden rule of CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bonus: Caching is used &lt;em&gt;everywhere&lt;/em&gt; (Python deps, Docker layers), slashing build times in half.&lt;/p&gt;

&lt;p&gt;This entire loop is automated, reproducible, and &lt;strong&gt;frictionless&lt;/strong&gt;. As long as your code is clean and your tests pass, your FastAPI service gets linted, tested, containerized, and published — all without touching your laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;In the Python world, where dependency hell, slow builds, and bloated containers are all too familiar, this stack — &lt;code&gt;uv&lt;/code&gt;, Docker multi-stage builds, &lt;code&gt;ruff&lt;/code&gt;, and GitHub Actions offer a modern antidote.&lt;/p&gt;

&lt;p&gt;Whether you’re working on internal tools, ML APIs, or full-fledged SaaS backends, the benefits are clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Faster iteration&lt;/li&gt;
&lt;li&gt;  Safer builds&lt;/li&gt;
&lt;li&gt;  Simpler team onboarding&lt;/li&gt;
&lt;li&gt;  Reproducibility across the board&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s next?
&lt;/h2&gt;

&lt;p&gt;Is that it? Not quite! We can further enhance our local and global CI/CD workflow. In the next installment, we’ll explore how to integrate tools like &lt;code&gt;uv&lt;/code&gt; (via its CLI or external CLIs like Poethepoet) and &lt;code&gt;nox&lt;/code&gt; to create an even more robust GitHub Actions pipeline. Stay tuned for the next episode!&lt;/p&gt;

&lt;p&gt;Happy shipping 🚀&lt;/p&gt;

</description>
      <category>python</category>
      <category>docker</category>
      <category>mlops</category>
      <category>devops</category>
    </item>
    <item>
      <title>Mastering Python Project Management with uv: Part 3 — MLops</title>
      <dc:creator>Thomas Bury</dc:creator>
      <pubDate>Thu, 10 Apr 2025 09:25:27 +0000</pubDate>
      <link>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-3-mlops-38e2</link>
      <guid>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-3-mlops-38e2</guid>
      <description>&lt;p&gt;Tired of waiting for pip? UV cuts dependency hell from coffee-break to blink-speed. Here's how I use it for MLOps—and why you should too.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;🔗 Originally published on &lt;a href="https://medium.com/p/4906965f45a2" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;. Shared here because dev.to folks know good tooling when they see it!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR (What’s In This Guide)&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🚀 UV is like pip on steroids&lt;/strong&gt;: Installs Python deps &lt;strong&gt;10x faster&lt;/strong&gt; than Poetry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔒 Lockfiles that work&lt;/strong&gt;: No more "works on my machine" nonsense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🐳 Docker magic&lt;/strong&gt;: Build slim images without the headache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🤖 CI/CD simplified&lt;/strong&gt;: GitHub Actions that don’t make you want to cry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;👉 Try it now&lt;/strong&gt;: &lt;a href="https://github.com/ThomasBury/mlops-uv/" rel="noopener noreferrer"&gt;mlops-uv&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;"why UV over Poetry/Pipenv/that-cool-tool-I-saw-on-HN?"&lt;/em&gt; Glad you asked. Let’s get hands-on.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use this guide
&lt;/h2&gt;

&lt;p&gt;The supporting repo: &lt;a href="https://github.com/ThomasBury/mlops-uv/" rel="noopener noreferrer"&gt;mlops-uv&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Build the project from scratch by manually setting up the structure and copy-pasting the provided code base (src and tests folders).&lt;/li&gt;
&lt;li&gt; Clone the repository, install dependencies using the command &lt;code&gt;uv sync\&lt;/code&gt;, and run the commands explained below directly to:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;  Execute the test suite&lt;/li&gt;
&lt;li&gt;  Build the Docker image&lt;/li&gt;
&lt;li&gt;  Modify and test GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;MLOps (Machine Learning Operations) is all about bringing DevOps principles into machine learning, making model deployment, versioning, and monitoring more efficient. However, managing dependencies, ensuring reproducibility, and streamlining deployments can be a major headache for ML/DS teams.&lt;/p&gt;

&lt;p&gt;That’s where &lt;strong&gt;UV&lt;/strong&gt; comes in — a fast, modern package manager that simplifies dependency management, build processes, and CI/CD for Python projects.&lt;/p&gt;

&lt;p&gt;In this article, we’ll explore how &lt;strong&gt;UV&lt;/strong&gt; can enhance MLOps workflows through &lt;strong&gt;AceBet&lt;/strong&gt;, a mock-up FastAPI app that predicts the winner of an ATP match (for demonstration purposes only — don’t bet your savings on it!). We’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Setting up a UV-based MLOps project&lt;/li&gt;
&lt;li&gt;  Managing dependencies and lockfiles&lt;/li&gt;
&lt;li&gt;  Automating CI/CD with GitHub Actions&lt;/li&gt;
&lt;li&gt;  Building and deploying with Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s dive in!&lt;/p&gt;

&lt;p&gt;Make sure to read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://bury-thomas.medium.com/mastering-python-project-management-with-uv-part1-its-time-to-ditch-poetry-c2590091d90a" rel="noopener noreferrer"&gt;uv part 1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://bury-thomas.medium.com/mastering-python-project-management-with-uv-part-2-deep-dives-and-advanced-use-1e2540e6f4a6" rel="noopener noreferrer"&gt;uv part2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;for a smoother reading of the part 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 Initializing an MLOps Project with UV
&lt;/h2&gt;

&lt;p&gt;When working on an MLOps project, structuring your codebase properly is crucial. We’ll start by setting up a &lt;a href="https://docs.astral.sh/uv/concepts/projects/init/#packaged-applications" rel="noopener noreferrer"&gt;&lt;strong&gt;packaged application&lt;/strong&gt;&lt;/a&gt; using UV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv init --package acebet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;packaged application&lt;/strong&gt; follows the &lt;strong&gt;src-based structure&lt;/strong&gt;, where the source code is contained within a dedicated package directory (&lt;code&gt;src/acebet&lt;/code&gt;). This approach is beneficial for:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Large applications with multiple modules&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Projects that need to be distributed (e.g., PyPI packages, CLI tools)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Better namespace isolation, preventing import conflicts&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Improved testability and modularity&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;example-pkg/
├── src/
│   ├── example_pkg/
│   │   ├── __init__.py
│   │   ├── module.py
│   │   └── utils.py
├── tests/
│   ├── test_module.py
├── pyproject.toml
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure ensures:&lt;br&gt;&lt;br&gt;
✔ &lt;strong&gt;Encapsulation&lt;/strong&gt;: The application is a &lt;strong&gt;proper Python package&lt;/strong&gt;, avoiding accidental name conflicts.&lt;br&gt;&lt;br&gt;
✔ &lt;strong&gt;Reusability&lt;/strong&gt;: Can be installed via &lt;code&gt;pip install .&lt;/code&gt; or published to PyPI.&lt;br&gt;&lt;br&gt;
✔ &lt;strong&gt;Cleaner Imports&lt;/strong&gt;: Enforces absolute imports (&lt;code&gt;from example_pkg.utils import foo&lt;/code&gt;) instead of relative imports.&lt;br&gt;&lt;br&gt;
✔ &lt;strong&gt;Better CI/CD Support&lt;/strong&gt;: Easier to package and distribute in Docker, PyPI, or GitHub Actions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flhmfxhp9a24lz17ru13y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flhmfxhp9a24lz17ru13y.png" alt="A rule of thumb for choosing between a regular py App and a packaged app" width="672" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 For quick scripts or internal projects? Use a regular application.&lt;br&gt;&lt;br&gt;
👉 For scalable, maintainable, and deployable projects? Use a packaged application.&lt;/p&gt;

&lt;p&gt;In our case, we will re-use AceBet, a simple FastAPI application following basic MLops principles. Including several modules for preparing the data, training the model, predicting and defining the endpoints, and a test suite.&lt;/p&gt;

&lt;p&gt;A packaged app is therefore the best choice (and will be for anything else than POC)&lt;/p&gt;
&lt;h2&gt;
  
  
  🔧 Managing Dependencies with UV
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Installing Core Dependencies
&lt;/h2&gt;

&lt;p&gt;Once your project is initialized, install the necessary dependencies for developing AceBet, including FastAPI and machine learning libraries like Scikit-learn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv add fastapi scikit-learn pandas lightgbm 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and any other packages required for the application to run properly. UV will take care of the resolution of the needed versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Lockfile for Reproducibility
&lt;/h2&gt;

&lt;p&gt;One of UV’s key advantages is &lt;strong&gt;ensuring dependency reproducibility&lt;/strong&gt; with a &lt;strong&gt;lockfile&lt;/strong&gt;. This guarantees that all environments (local, staging, production) use the same dependency versions.&lt;/p&gt;

&lt;p&gt;Once you are satisfied with the first version of the code base, generate a lockfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you want to sync all dependencies in one go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This process ensures that dependency versions remain consistent across different environments — an essential practice in MLOps.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 Adding Testing Dependencies &amp;amp; Running Tests
&lt;/h2&gt;

&lt;p&gt;In MLOps, &lt;strong&gt;testing&lt;/strong&gt; is just as important as model accuracy. UV provides 3 different and very convenient ways to run tests or use tools (a tool is usually a Python CLI such as pytest):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzsguuf8dxhoi4oxc1bjs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzsguuf8dxhoi4oxc1bjs.png" alt="Three ways of using development dependencies in UV" width="700" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  If you need a tool &lt;strong&gt;as part of your Python project&lt;/strong&gt;, &lt;strong&gt;add it as a dependency&lt;/strong&gt; (&lt;code&gt;uv add --dev&lt;/code&gt;), they will be listed in the &lt;code&gt;pyproject.toml&lt;/code&gt; as using other project managers.&lt;/li&gt;
&lt;li&gt;  If you only need to &lt;strong&gt;run a tool occasionally&lt;/strong&gt;, &lt;strong&gt;execute it with&lt;/strong&gt; &lt;code&gt;uvx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  If you need a tool &lt;strong&gt;persistently in your system or Docker&lt;/strong&gt;, &lt;strong&gt;install it using&lt;/strong&gt; &lt;code&gt;uv tool install&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can add testing libraries using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv add --dev pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These dependencies will then be distributed as development dependencies with your application distribution.&lt;/p&gt;

&lt;p&gt;A final piece of advice on how to choose the method:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0e0ib0n3us9vp3wbeuzx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0e0ib0n3us9vp3wbeuzx.png" alt="Comparison of the two methods for installing the dependencies (uvx does not install them at all)" width="700" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pyproject.toml&lt;/code&gt; should explicitly list all required dependencies for reliable test suite distribution, guaranteeing that developers use the same versions. We opt for the &lt;code&gt;uv add --dev&lt;/code&gt; method, but the &lt;code&gt;uv tool install&lt;/code&gt; will come in handy for Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Automating CI/CD with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Now that our application is running and tested properly, we want to ensure integrity if new commits are merged to the main branch.&lt;/p&gt;

&lt;p&gt;A robust &lt;strong&gt;CI/CD pipeline&lt;/strong&gt; ensures your models and applications are always production-ready. With UV, setting up GitHub Actions is straightforward.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.astral.sh/" rel="noopener noreferrer"&gt;Astral&lt;/a&gt; provides a &lt;strong&gt;GitHub Actions workflow&lt;/strong&gt; that installs dependencies and runs tests automatically on every push to the &lt;code&gt;main&lt;/code&gt; branch. A simple example would be running the test suite for each new commit on the main branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Testing&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;uv-example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Python&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install UV&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v5&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install the project&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv sync --all-extras --dev&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run pytest tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow:&lt;br&gt;&lt;br&gt;
✅ Installs UV&lt;br&gt;&lt;br&gt;
✅ Syncs dependencies&lt;br&gt;&lt;br&gt;
✅ Runs unit tests using Pytest&lt;/p&gt;

&lt;p&gt;You can refine and add a Python matrix, and &lt;a href="https://docs.astral.sh/uv/guides/integration/github/" rel="noopener noreferrer"&gt;further sophistication&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🐳 Building a Docker Image with UV
&lt;/h2&gt;

&lt;p&gt;A well-built &lt;strong&gt;Docker image&lt;/strong&gt; simplifies deployment and ensures your application runs consistently in any environment. UV makes it easy to containerize an application.&lt;/p&gt;

&lt;p&gt;Here’s a &lt;strong&gt;basic Dockerfile&lt;/strong&gt; to containerize AceBet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="c"&gt;# Install UV&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/&lt;/span&gt;

&lt;span class="c"&gt;# Copy the application into the container&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;

&lt;span class="c"&gt;# Set working directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt;

&lt;span class="c"&gt;# Run the FastAPI app&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/app/.venv/bin/fastapi", "run", "src/acebet/app/main.py", "--port", "80", "--host", "0.0.0.0"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;strong&gt;production-ready builds&lt;/strong&gt;, use a &lt;a href="https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers" rel="noopener noreferrer"&gt;&lt;strong&gt;multi-stage Docker build&lt;/strong&gt;&lt;/a&gt; to keep the final image lightweight.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌟 Why UV for MLOps?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2rtq6wnzbn194su6wzom.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2rtq6wnzbn194su6wzom.png" alt="Comparison to Poetry, a widely used project manager" width="700" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Conclusion
&lt;/h2&gt;

&lt;p&gt;By integrating UV into your MLOps workflow, you get a &lt;strong&gt;fast, reproducible, and efficient&lt;/strong&gt; setup for managing dependencies, testing, and deployment.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;AceBet&lt;/strong&gt;, we demonstrated how to:&lt;br&gt;&lt;br&gt;
✔️ Initialize a structured UV project&lt;br&gt;&lt;br&gt;
✔️ Manage dependencies &amp;amp; lockfiles&lt;br&gt;&lt;br&gt;
✔️ Automate testing with GitHub Actions&lt;br&gt;&lt;br&gt;
✔️ Build Docker images for deployment&lt;/p&gt;

&lt;p&gt;If you’re working with Python-based MLOps projects, &lt;strong&gt;give UV a try&lt;/strong&gt; — it might just replace Pip and Poetry in your workflow! 🚀&lt;/p&gt;

</description>
      <category>python</category>
      <category>mlops</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Mastering Python Project Management with uv: Part 2 – Deep Dives and Advanced Use</title>
      <dc:creator>Thomas Bury</dc:creator>
      <pubDate>Sun, 06 Oct 2024 08:00:01 +0000</pubDate>
      <link>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-2-deep-dives-and-advanced-use-4mlb</link>
      <guid>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part-2-deep-dives-and-advanced-use-4mlb</guid>
      <description>&lt;p&gt;Welcome back! If you haven’t checked out &lt;a href="https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part1-its-time-to-ditch-poetry-3bi0"&gt;Part 1&lt;/a&gt;, now’s the time to do so. We covered how &lt;code&gt;uv&lt;/code&gt; simplifies managing dependencies, creating virtual environments, Python versions, and inline metadata. Now, let's go deeper into the magic of &lt;code&gt;uv&lt;/code&gt; and explore its advanced features that can take Python development to the next level.&lt;/p&gt;

&lt;p&gt;🛠 Updated on 23rd Feb 2025&lt;/p&gt;

&lt;p&gt;This article has been revised based on valuable feedback from readers, thank you all!&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Managing Dependencies with &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In Part 1, we introduced adding dependencies using &lt;code&gt;uv&lt;/code&gt;. Let's expand on how &lt;code&gt;uv&lt;/code&gt; integrates seamlessly with &lt;code&gt;pyproject.toml&lt;/code&gt;, the standardized configuration file for Python projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Dependencies with &lt;code&gt;uv add&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When you add a dependency using &lt;code&gt;uv add&lt;/code&gt;, it updates your &lt;code&gt;pyproject.toml&lt;/code&gt; file accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add fastapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command modifies your &lt;code&gt;pyproject.toml&lt;/code&gt; to include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your_project_name"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"fastapi&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes your dependencies explicit and your project easily shareable with others. Whenever you or someone else clones your project, simply running &lt;code&gt;uv&lt;/code&gt; will install the same packages listed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Constraints: &lt;code&gt;&amp;gt;=&lt;/code&gt; vs. &lt;code&gt;==&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;It’s advisable to use version ranges (e.g., &lt;code&gt;&amp;gt;=0.68,&amp;lt;1.0&lt;/code&gt;) rather than strict pinning (&lt;code&gt;==0.68&lt;/code&gt;). This approach allows for compatibility with newer, non-breaking versions, ensuring your project remains up-to-date without unexpected disruptions. Strict pinning can lead to dependency conflicts and hinder the integration of security patches or performance improvements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional Dependencies and Dependency Groups
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; supports the standardized &lt;code&gt;optional-dependencies&lt;/code&gt; and &lt;code&gt;dependency-groups&lt;/code&gt; as per the latest PEPs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project.optional-dependencies]&lt;/span&gt;
&lt;span class="py"&gt;dev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["pytest&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="err"&gt;black&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="nn"&gt;[tool.uv.dependency-groups.docs]&lt;/span&gt;
&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["sphinx&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;optional&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To install these groups, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add &lt;span class="nt"&gt;--group&lt;/span&gt; dev &amp;lt;DEV_PACKAGE_01&amp;gt; &amp;lt;DEV_PACKAGE_02&amp;gt;
uv add &lt;span class="nt"&gt;--group&lt;/span&gt; docs &amp;lt;DOCS_PACKAGE_01&amp;gt; &amp;lt;DOCS_PACKAGE_02&amp;gt; &amp;lt;DOCS_PACKAGE_03&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for development dependencies and documentation dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Locking Dependencies with the &lt;code&gt;uv.lock&lt;/code&gt; File
&lt;/h2&gt;

&lt;p&gt;Whenever you add or update dependencies using &lt;code&gt;uv&lt;/code&gt;, it doesn’t just modify your &lt;code&gt;pyproject.toml&lt;/code&gt; file. &lt;code&gt;uv&lt;/code&gt; also creates a &lt;code&gt;uv.lock&lt;/code&gt; file. Why is this important?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precise Versioning&lt;/strong&gt;: The &lt;code&gt;uv.lock&lt;/code&gt; file locks in the exact versions of all dependencies and their transitive dependencies (packages that your dependencies rely on).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproducible Environments&lt;/strong&gt;: Whether it’s you coming back to a project after a break or a colleague cloning your repo, running &lt;code&gt;uv&lt;/code&gt; will install the exact versions specified in the &lt;code&gt;uv.lock&lt;/code&gt; file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? A consistent, reliable environment that eliminates the “it works on my machine” problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Managing Tools: Global vs. Project-Specific
&lt;/h2&gt;

&lt;p&gt;In Part 1, we discussed how &lt;code&gt;uv&lt;/code&gt; makes it easy to install CLI tools. Now, let’s break down how &lt;code&gt;uv&lt;/code&gt; distinguishes between global and project-specific tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Global Tools
&lt;/h3&gt;

&lt;p&gt;Installing a tool globally with &lt;code&gt;uv&lt;/code&gt; is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;black
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes &lt;code&gt;black&lt;/code&gt; available across all your projects but keeps it in its isolated virtual environment, avoiding system-wide conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project-Specific Tools
&lt;/h3&gt;

&lt;p&gt;If you need a tool for a specific project, add it directly as a dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add &lt;span class="nt"&gt;--group&lt;/span&gt; dev ruff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the tool local to your project and listed in your &lt;code&gt;pyproject.toml&lt;/code&gt;. Your other projects remain unaffected, allowing for isolated development environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Tools Ephemerally with &lt;code&gt;uvx&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;For quick, one-off tool usage without permanently installing it, &lt;code&gt;uvx&lt;/code&gt; is your friend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx black my_script.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs &lt;code&gt;black&lt;/code&gt; within a temporary virtual environment and then cleans up afterward.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Creating &amp;amp; Using Virtual Environments the Right Way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Making Virtual Environments Easy
&lt;/h3&gt;

&lt;p&gt;In Part 1, we explained how &lt;code&gt;uv&lt;/code&gt; defaults to using virtual environments for all package installations. Here’s a quick recap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command creates a &lt;code&gt;.venv&lt;/code&gt; directory in your project. If you want to use a custom directory or Python version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv my_venv - python 3.11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then activate your virtual environment as you normally would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;.venv/Scripts/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Automatic Environment Detection
&lt;/h3&gt;

&lt;p&gt;Whenever you work on a project managed by &lt;code&gt;uv&lt;/code&gt;, the tool will automatically detect and use the appropriate virtual environment without any additional setup. No need to worry about manually activating or deactivating environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. &lt;code&gt;uv&lt;/code&gt; and Existing Environments
&lt;/h2&gt;

&lt;p&gt;Already using another environment manager like &lt;code&gt;conda&lt;/code&gt;? No problem. &lt;code&gt;uv&lt;/code&gt; is built to play nicely with external virtual environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Environment Detection &amp;amp; Integration
&lt;/h3&gt;

&lt;p&gt;When you use &lt;code&gt;uv pip install&lt;/code&gt; or &lt;code&gt;uv add&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt; searches for existing virtual environments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Activated environments (e.g., &lt;code&gt;$VIRTUAL_ENV&lt;/code&gt; or &lt;code&gt;$CONDA_PREFIX&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.venv&lt;/code&gt; directory in your current project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If &lt;code&gt;uv&lt;/code&gt; doesn’t find a virtual environment, it will prompt you to create one to keep your environment clean and isolated.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Using Alternative Package Indexes and Authentication
&lt;/h2&gt;

&lt;p&gt;While &lt;code&gt;uv&lt;/code&gt; defaults to the official Python Package Index (PyPI), it supports alternative indexes, which often require authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Alternative Indexes
&lt;/h3&gt;

&lt;p&gt;Set an alternative index URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;UV_INDEX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://example.com/simple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Authentication Using Environment Variables
&lt;/h3&gt;

&lt;p&gt;For indexes requiring authentication, provide credentials via environment variables. For instance, with Azure Artifacts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;UV_INDEX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://username:&lt;span class="nv"&gt;$ADO_PAT&lt;/span&gt;@pkgs.dev.azure.com/organization/project/_packaging/feedName/pypi/simple/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;$ADO_PAT&lt;/code&gt; with your Personal Access Token. Refer to the official &lt;code&gt;uv&lt;/code&gt; documentation for detailed instructions on various services.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Permanent Configuration
&lt;/h2&gt;

&lt;p&gt;To avoid setting environment variables repeatedly, configure &lt;code&gt;uv&lt;/code&gt; persistently.&lt;/p&gt;

&lt;p&gt;Within a project, add the index URL to &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv]&lt;/span&gt;
&lt;span class="py"&gt;index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/simple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, use &lt;code&gt;uv.toml&lt;/code&gt; (preferred for non-Python-specific configurations):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[index]]&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/simple"&lt;/span&gt;
&lt;span class="py"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Precedence Order:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;uv.toml&lt;/code&gt; overrides &lt;code&gt;pyproject.toml&lt;/code&gt; if both exist in the same directory.&lt;/li&gt;
&lt;li&gt;Project-level settings override user-level settings, which override system-level settings.&lt;/li&gt;
&lt;li&gt;Environment variables take priority over all configuration files.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;uv --no-config&lt;/code&gt; to temporarily ignore all configurations.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  8. Types of Projects and Build System Options
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; supports multiple project types, each with different build system configurations. Selecting the right configuration depends on your project's packaging and distribution needs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Project Type&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Example &lt;code&gt;pyproject.toml&lt;/code&gt; Config&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard Python Project&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;setuptools&lt;/code&gt; for traditional package builds. Ideal for legacy and widely adopted setups.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[build-system] requires = ["setuptools", "wheel"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PEP 517 Build Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines a custom backend like &lt;code&gt;hatchling&lt;/code&gt;, &lt;code&gt;poetry&lt;/code&gt;, or &lt;code&gt;flit&lt;/code&gt;. Recommended for modern Python packaging.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[build-system] requires = ["hatchling"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workspace (Multi-Package)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A single repository containing multiple projects. Useful for monorepos and modular architectures.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[tool.uv] workspace = true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Non-Build Projects&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scripts or tool configurations that do not require packaging. Used for CLI utilities and personal projects.&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;[build-system]&lt;/code&gt; section needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;🔹 &lt;strong&gt;Best Practice&lt;/strong&gt;:&lt;br&gt;&lt;br&gt;
For new projects, &lt;strong&gt;use &lt;code&gt;hatchling&lt;/code&gt; or &lt;code&gt;flit&lt;/code&gt;&lt;/strong&gt; instead of &lt;code&gt;setuptools&lt;/code&gt;, as they align with modern Python packaging standards.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Next?
&lt;/h2&gt;

&lt;p&gt;✔ &lt;strong&gt;New to &lt;code&gt;uv&lt;/code&gt;?&lt;/strong&gt; Start with &lt;a href="https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part1-its-time-to-ditch-poetry-3bi0"&gt;Part 1&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
✔ &lt;strong&gt;Want more details?&lt;/strong&gt; Check out the official &lt;code&gt;uv&lt;/code&gt; documentation.&lt;/p&gt;

&lt;p&gt;💬 Have you tried &lt;code&gt;uv&lt;/code&gt; yet? Share your experience and questions in the comments! 🐍✨&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>devops</category>
      <category>python</category>
      <category>softwaredevelopment</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Mastering Python Project Management with UV (Part 1): It’s Time to Ditch Poetry</title>
      <dc:creator>Thomas Bury</dc:creator>
      <pubDate>Sun, 29 Sep 2024 14:38:21 +0000</pubDate>
      <link>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part1-its-time-to-ditch-poetry-3bi0</link>
      <guid>https://dev.to/thomas_bury_b1a50c1156cbf/mastering-python-project-management-with-uv-part1-its-time-to-ditch-poetry-3bi0</guid>
      <description>&lt;p&gt;🛠 Updated on 23rd Feb 2025&lt;br&gt;
This article has been revised based on valuable feedback from readers, thank you all!&lt;/p&gt;

&lt;p&gt;Are you tired of juggling multiple tools like &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;virtualenv&lt;/code&gt;, &lt;code&gt;conda&lt;/code&gt;, &lt;code&gt;poetry&lt;/code&gt;, and &lt;code&gt;pyenv&lt;/code&gt; just to keep your Python environments and dependencies in check? You’re not alone! Managing Python projects can feel like a headache, especially with all the different package managers and tools you have to wrangle.&lt;/p&gt;
&lt;h2&gt;
  
  
  Enter &lt;code&gt;uv&lt;/code&gt; – The Universal Virtualenv
&lt;/h2&gt;

&lt;p&gt;Think of &lt;code&gt;uv&lt;/code&gt; as a &lt;strong&gt;one-stop-shop package manager&lt;/strong&gt; designed to streamline and speed up your Python development process.&lt;/p&gt;


&lt;h2&gt;
  
  
  A Little Backstory
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; draws its inspiration from &lt;strong&gt;Rye&lt;/strong&gt;, another modern packaging manager, to unify the best features of &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;pip-tools&lt;/code&gt;, &lt;code&gt;pyenv&lt;/code&gt;, &lt;code&gt;virtualenv&lt;/code&gt;, and &lt;code&gt;poetry&lt;/code&gt;. Built using Rust, &lt;code&gt;uv&lt;/code&gt; is not just fast but highly efficient, simplifying everything from managing dependencies to creating virtual environments.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Aim of &lt;code&gt;uv&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In a nutshell, &lt;code&gt;uv&lt;/code&gt; is about &lt;strong&gt;consolidation&lt;/strong&gt;. Why switch between multiple tools when you can have one unified experience? It aims to remove the friction from Python development, offering you a more consistent and faster way to manage your projects. And it’s also &lt;strong&gt;blazing fast!&lt;/strong&gt; That opens new doors for &lt;a href="https://www.youtube.com/watch?v=jXWIxk2brfk" rel="noopener noreferrer"&gt;dynamic management&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Portable Code with Inline Script Metadata
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Let’s Talk Dependencies
&lt;/h3&gt;

&lt;p&gt;One of the most exciting features of &lt;code&gt;uv&lt;/code&gt; is the ability to add dependencies &lt;strong&gt;directly within your Python script&lt;/strong&gt;. Imagine you have a simple script like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rich.pretty&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pprint&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://peps.python.org/api/peps.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()][:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this script &lt;strong&gt;usually&lt;/strong&gt; means setting up a virtual environment and installing dependencies manually. &lt;strong&gt;With &lt;code&gt;uv&lt;/code&gt;, you can embed all your dependencies directly into the script&lt;/strong&gt;, making it self-contained and shareable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add &lt;span class="nt"&gt;--script&lt;/span&gt; app.py &lt;span class="s1"&gt;'requests&amp;lt;3'&lt;/span&gt; &lt;span class="s1"&gt;'rich'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Automatic Metadata Generation&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This adds metadata to the script file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py
# /// script
# dependencies = [
#   "requests&amp;lt;3",
#   "rich",
# ]
# ///
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rich.pretty&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pprint&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://peps.python.org/api/peps.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()][:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s it! You can &lt;strong&gt;share this file with someone else&lt;/strong&gt;, and they can simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And voilà — &lt;strong&gt;no external setup required!&lt;/strong&gt; All thanks to &lt;code&gt;uv&lt;/code&gt;’s speed and efficiency.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Creating and Managing Virtual Environments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Getting Started with Virtual Environments
&lt;/h3&gt;

&lt;p&gt;By default, &lt;code&gt;uv&lt;/code&gt; requires packages to be installed within &lt;strong&gt;virtual environments&lt;/strong&gt; to keep your system clean and avoid conflicts between different projects. &lt;strong&gt;Creating a virtual environment with &lt;code&gt;uv&lt;/code&gt; is simple:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a &lt;code&gt;.venv&lt;/code&gt; directory containing the isolated environment. If you want to specify a &lt;strong&gt;custom directory or Python version&lt;/strong&gt;, you can do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv my_env &lt;span class="nt"&gt;--python&lt;/span&gt; 3.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The environment is ready to use, and &lt;code&gt;uv&lt;/code&gt; will &lt;strong&gt;detect it automatically&lt;/strong&gt; for all your commands, like installing packages or running scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Use &lt;code&gt;uv add&lt;/code&gt; vs. &lt;code&gt;uv pip install&lt;/code&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  ✅ &lt;strong&gt;Use &lt;code&gt;uv add&lt;/code&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;When you want to &lt;strong&gt;add dependencies to your project’s &lt;code&gt;pyproject.toml&lt;/code&gt; file&lt;/strong&gt;. This is best when you are &lt;strong&gt;developing a project&lt;/strong&gt; and want to keep track of all dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add fastapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will &lt;strong&gt;update your &lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/strong&gt; and lock the version in &lt;code&gt;uv.lock&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  ✅ &lt;strong&gt;Use &lt;code&gt;uv pip install&lt;/code&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;When you want to install packages for &lt;strong&gt;quick use without modifying the project file&lt;/strong&gt; or for global tools where you don’t need to track them in a &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Choosing the right command ensures your project is properly managed and easy to share or deploy.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Lock Versions for Reproducibility
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ever Had Your Code Break Due to Updates?
&lt;/h3&gt;

&lt;p&gt;We’ve all been there — your code works today, then &lt;strong&gt;breaks tomorrow&lt;/strong&gt; because a package gets updated. With &lt;code&gt;uv&lt;/code&gt;, you can &lt;strong&gt;prevent this&lt;/strong&gt; by locking package versions to ensure &lt;strong&gt;consistency and reproducibility&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv]&lt;/span&gt;
&lt;span class="py"&gt;exclude-newer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2023-10-16T00:00:00Z"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, even if &lt;strong&gt;new versions&lt;/strong&gt; of your dependencies come out, &lt;strong&gt;your project remains stable&lt;/strong&gt;. Perfect for long-term projects where you &lt;strong&gt;can’t afford surprises!&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Managing Python Versions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Different Projects, Different Python Versions? No Problem!
&lt;/h3&gt;

&lt;p&gt;Many developers work on multiple projects that require &lt;strong&gt;different Python versions&lt;/strong&gt;. &lt;code&gt;uv&lt;/code&gt; makes switching versions as easy as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv python &lt;span class="nb"&gt;install &lt;/span&gt;3.8 3.9 3.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the versions are installed, &lt;strong&gt;switching between them is seamless&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run &lt;span class="nt"&gt;--python&lt;/span&gt; 3.10 app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you want to &lt;strong&gt;lock a specific version for a project&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv python pin 3.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No more juggling &lt;code&gt;pyenv&lt;/code&gt; commands&lt;/strong&gt; — &lt;code&gt;uv&lt;/code&gt; handles all the heavy lifting for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Say Goodbye to &lt;code&gt;pip&lt;/code&gt; Hassles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It’s &lt;code&gt;pip&lt;/code&gt; — but Faster and Better
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; provides a &lt;strong&gt;&lt;code&gt;pip&lt;/code&gt;-like experience&lt;/strong&gt;, but with &lt;strong&gt;turbocharged performance&lt;/strong&gt;. Installing packages is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv pip &lt;span class="nb"&gt;install &lt;/span&gt;flask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need to add &lt;strong&gt;optional dependencies&lt;/strong&gt; or install directly from a &lt;strong&gt;GitHub repo&lt;/strong&gt;? No sweat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'torch&amp;gt;=1.10.0'&lt;/span&gt; &lt;span class="s2"&gt;"git+https://github.com/astral-sh/ruff"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more &lt;strong&gt;waiting around&lt;/strong&gt; for slow installations — &lt;code&gt;uv&lt;/code&gt; gets the job done &lt;strong&gt;fast and effectively&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Manage CLI Tools Globally and Easily
&lt;/h2&gt;

&lt;h3&gt;
  
  
  From &lt;code&gt;black&lt;/code&gt; to &lt;code&gt;ruff&lt;/code&gt;, Get Your Tools Hassle-Free
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Globally:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;ruff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Locally within a Project:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add ruff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Run Ephemeral Commands without Installing Globally:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx black my_code.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Say goodbye to &lt;strong&gt;package conflicts and environment pollution&lt;/strong&gt; — just &lt;strong&gt;run your tools whenever and wherever you need them&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Ready to Take &lt;code&gt;uv&lt;/code&gt; for a Spin?
&lt;/h2&gt;

&lt;p&gt;If you’re looking to &lt;strong&gt;supercharge your Python development&lt;/strong&gt; and want to stop wrestling with multiple tools, &lt;code&gt;uv&lt;/code&gt; is your answer.  &lt;/p&gt;

&lt;p&gt;With its &lt;strong&gt;streamlined commands&lt;/strong&gt;, &lt;strong&gt;reproducible environments&lt;/strong&gt;, and &lt;strong&gt;efficient package management&lt;/strong&gt;, &lt;code&gt;uv&lt;/code&gt; makes Python development &lt;strong&gt;a pleasure rather than a chore&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;📌 &lt;strong&gt;Stay tuned for Part 2&lt;/strong&gt;, where we’ll dive deeper into &lt;strong&gt;advanced features&lt;/strong&gt; like leveraging &lt;code&gt;pyproject.toml&lt;/code&gt;, handling &lt;strong&gt;global vs. local tool installations&lt;/strong&gt;, and how &lt;code&gt;uv&lt;/code&gt; can be your &lt;strong&gt;best friend&lt;/strong&gt; when managing complex environments.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;For full details and documentation, check out &lt;a href="https://docs.astral.sh/uv" rel="noopener noreferrer"&gt;&lt;code&gt;uv&lt;/code&gt; documentation&lt;/a&gt;.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;🐍✨ &lt;strong&gt;Happy coding!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
