Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
Labs

Python Packaging and Dependency Management in the AI Era

Python dependency management has changed. The old loop of pip, hand-edited requirements.txt files and fragile virtual environments is giving way to uv, a single fast tool that scaffolds projects, resolves dependencies, and locks them for perfect reproducibility. In this hands-on Code Lab, you take over a neglected Globomantics command-line utility and rebuild it the modern way. You scaffold the project with uv, migrate its legacy dependencies, separate runtime and development packages, generate a lock file, and inspect and debug the dependency graph when a version conflict blocks resolution. You then step into the AI era: an AI advisor recommends a package and you learn to validate that recommendation against the resolver and lock file before trusting it. By the end, you can manage real Python projects with uv and judge AI-generated dependency advice with confidence.

Lab platform
Lab Info
Level
Advanced
Last updated
Jun 18, 2026
Duration
30m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Introduction

    Python dependency management has changed. For years the routine was pip, a hand-edited requirements.txt and a virtual environment you created and activated yourself. That workflow is slow, easy to get wrong, and almost impossible to reproduce exactly on another machine. uv replaces all of it with a single fast tool that scaffolds a project, resolves and installs dependencies, separates runtime packages from development packages, and locks every version for perfect reproducibility. Learning uv is one of the highest-leverage upgrades you can make to a modern Python workflow.

    You join Globomantics as a back-end developer and inherit datakit, a small command-line utility that cleans and summarises sales CSV files. It arrives as a loose folder with a legacy requirements.txt and no real packaging. Your job across this lab is to rebuild it into a modern, reproducible uv project.

    The workspace is pre-configured so you can focus on packaging. Lab environment doesn't have an actual network access (aside from the exception of LLM APIs).

    • legacy/requirements.txt is the old-style dependency list the previous team left behind. You migrate it into the project, but you do not edit it.
    • legacy/README.md describes the old pip and venv workflow, for comparison. You do not edit it.
    • tools/ai_advisor.py is a ready-made script that asks the lab's AI advisor for one package to use for fast CSV parsing and prints its name. You run it later, but you do not edit it.
    • sample_data/sales.csv is a small sample file the utility reads, so the project has something to run against. You do not edit it.
    • .env holds a LAB_API_KEY= line. You paste your key into this one line before running the AI advisor.

    There is deliberately no pyproject.toml, no uv.lock, and no .venv yet, because creating the project is the first thing you do.

    Each task can be validated individually by clicking on the Validate button next to it.

    If you get stuck, every task has a Task Solution section you can expand to reveal the answer. This can be found under the FEEDBACK/CHECKS section from every task.

    info> The solutions/ folder at the top of the workspace contains the final state of every task.

    A failed task will list one or more failed checks under its Checks section, each with a specific message describing what went wrong.

    The starting point of the lab is a directory named datakit-workspace. The current directory of the built-in terminal will be set to the datakit-workspace directory. uv is already installed and on the PATH. The anthropic SDK and python-dotenv are already installed with pip3 so the AI advisor script runs on its own, before any project environment exists.

    Note: Before you run the AI advisor later in the lab, copy the API key from the top of the Code Lab menu and paste it into the .env file as the value of LAB_API_KEY.

    You use the Terminal to run every uv command in this lab.

    Click on the Next step arrow to get started.

  2. Challenge

    Scaffold a modern uv project

    The traditional Python workflow spreads one job across several tools. You create a virtual environment with python3 -m venv .venv, activate it, install packages with pip install, then freeze the result into a requirements.txt by hand. Nothing records the exact versions that resolved, so the same requirements.txt can install different versions tomorrow or on a teammate's machine. There is no single file that describes the project, and runtime packages sit in the same flat list as tools you only need while developing.

    uv collapses that whole loop into one tool. A single command scaffolds a project, and from then on uv manages the environment, the dependencies and their exact versions for you. The project is described by one file, pyproject.toml, which follows the modern Python standard and replaces both setup.py and requirements.txt.

    You start a new uv project by running uv init with a project name. For an unrelated example:

    uv init weather-cli
    

    This creates a weather-cli directory containing a ready-to-use pyproject.toml, so the project is a real package from the very first command. Running uv init datakit created a datakit/ subfolder, and that folder is now the home of the project. Every later uv command in this lab runs from inside it, so change into it once with cd datakit before you continue.

    Open datakit/pyproject.toml and look at the generated [project] table. It describes the project in one place:

    [project]
    name = "datakit"
    version = "0.1.0"
    description = "Add your description here"
    readme = "README.md"
    requires-python = ">=3.10"
    dependencies = []
    

    The name and version identify the package. requires-python declares which interpreter versions the project supports. The dependencies array is the list of packages the project needs at runtime, and it starts empty. This single standard file does the job that setup.py and requirements.txt used to split between them. The datakit folder is now a real uv package. A single uv init gave it a standard pyproject.toml and the legacy requirements.txt became managed entries in the project's dependencies array. What used to be a loose folder with a hand-edited list is now described by one modern file that uv understands end to end.

  3. Challenge

    Manage dependencies and environments

    A project rarely needs the same packages at runtime as it does during development. The code that ships depends on libraries it calls when it runs, while test runners, linters and formatters are only ever used while you build the project. Keeping these two sets apart means a deployment installs only what it actually needs.

    uv gives you one command for each move. uv add records a runtime dependency in the dependencies array. uv add --dev records a development-only dependency in a separate dependency group, so it never ships as part of the runtime requirements. uv remove deletes a dependency you no longer want and updates the project for you.

    For example, if you had a weather-cli project you might wire up a runtime client, a dev-only test runner and then drop a package you stopped using:

    uv add requests
    uv add --dev pytest
    uv remove requests
    

    Each command edits pyproject.toml for you, so the runtime list and the development group always reflect the real shape of the project. The project now draws a clean line between runtime and development needs. typer sits in the runtime dependencies array because the utility calls it when it runs, pytest lives in a separate development dependency group because it is only used while building the project, and requests is gone now that httpx covers every HTTP call. The dependency set in datakit/pyproject.toml reflects what the project actually requires, nothing more.

  4. Challenge

    Lock for reproducible environments

    A pyproject.toml declares what a project wants. It says a package must be at least some version or must stay within a range, but it does not record which exact build was installed. Two people resolving the same pyproject.toml weeks apart can end up with different versions as new releases land. A lock file closes that gap. It records the exact resolved version of every package, direct and transitive, so a teammate who installs from it gets a byte-for-byte match of your environment.

    uv splits these two roles across two files. pyproject.toml holds your intent as version specifiers, and uv.lock holds the resolved result that uv lock computes from that intent. A version specifier controls how much room the resolver has. ==2.1.0 pins one exact version, >=2.1 allows that version or anything newer and ~=2.1 allows patch and minor updates inside the 2.x line while holding the major version steady.

    For example, a weather-cli project might loosen one dependency to a known-good floor while keeping the others open:

    dependencies = [
        "pendulum>=3.0",
        "click",
    ]
    

    Here pendulum must resolve to 3.0 or newer while click is free to resolve to whatever the resolver picks. Running uv lock then turns those specifiers into a fully pinned uv.lock. The project now carries both halves of a reproducible setup. datakit/pyproject.toml states the intent with a pinned httpx>=0.27 floor, and datakit/uv.lock records the exact versions the resolver chose for every direct and transitive package. Running uv sync turned that lock file into a real .venv on disk. These two files are what teammates commit and share, because anyone who checks them out and runs uv sync rebuilds the identical environment.

  5. Challenge

    Analyze and debug the dependency graph

    When you add packages to a project you usually name only the ones you call directly. Those are your direct dependencies. Each of them often pulls in packages of its own, and those pull in more, and so on down the chain. Everything below your direct dependencies is a transitive dependency. The flat dependencies array in pyproject.toml lists only the direct packages, so it hides most of what is actually installed.

    A dependency tree makes the full picture visible. It prints each direct dependency and nests the packages it requires underneath, level by level, so you can see why a package you never named ended up in your environment. uv prints this graph with uv tree.

    For example, a small notes-cli project that depends only on requests might produce a tree like this:

    notes-cli v0.1.0
    └── requests v2.32.3
        ├── certifi v2024.8.30
        ├── charset-normalizer v3.4.0
        ├── idna v3.10
        └── urllib3 v2.2.3
    

    Here requests is the only direct dependency and the four packages nested under it are transitive. They arrived because requests needs them, not because the project asked for them. A version conflict happens when two constraints in the same resolution cannot both be true at once. The resolver walks every direct and transitive requirement and tries to find one version of each package that satisfies all of them together. When no such combination exists, resolution fails instead of guessing.

    Imagine a project that pins one package directly while a second package quietly requires a newer release of that same dependency:

    dependencies = [
        "pillow==9.0",
        "thumbnailer",
    ]
    

    If thumbnailer requires pillow>=10, the two constraints collide. The resolver cannot pick a single pillow that is both exactly 9.0 and at least 10, so uv lock stops and prints a resolver error naming the packages and the versions it could not reconcile. Reading that message tells you which constraint to relax.

    The datakit project already carries typer, and typer needs a recent rich. That makes it a safe place to watch a real conflict happen. Pinning rich to an old release that predates what typer requires forces uv lock to fail, and relaxing that pin lets resolution succeed again. You can now read the dependency graph and trace a failure back to its cause. uv tree showed how each direct dependency branches into the transitive packages underneath it, so a package you never named has a visible reason for being there. Pinning rich to the incompatible rich==10.0 made uv lock fail, and the resolver error pointed straight at the constraint that could not be satisfied. Relaxing that pin to rich>=13 and re-running uv lock let resolution succeed again with a rich that both your project and typer accept.

  6. Challenge

    Validate AI-suggested dependencies

    AI coding assistants are good at suggesting libraries, but they answer with the same confidence whether they are right or wrong. An assistant can invent a package name that was never published, point you at a project that was abandoned years ago or propose a version that no resolver can install. The name sounds plausible, so it is easy to paste it straight into a project and only discover the problem later.

    The safe habit is to treat every AI dependency suggestion as a proposal, not a decision. A proposal has to clear the same bar as any other dependency. The package has to exist on the index, its metadata has to resolve against everything else the project already requires and it has to land in a lock file with a real version. The resolver is what confirms all of that, so the resolver is what you trust, not the assistant.

    For example, an assistant asked for an HTTP client might reply with a single name like this:

    zoomrequests
    

    That answer is only useful once a tool has confirmed the package is real and compatible. Until then it is a guess.

    The tools/ai_advisor.py script in the workspace does exactly this kind of ask. It sends the lab's model a short request for one package name suited to a stated need and prints whatever name the model returns, with no validation of its own.

    Note: Before you run the AI advisor later in the lab, copy the API key from the top of the Code Lab menu and paste it into the .env file as the value of LAB_API_KEY. A printed package name is still just a proposal. Validating it means handing it to the resolver and letting the lock file decide. When you run uv add with the suggested name, uv looks the package up on the index, resolves its requirements against everything the project already declares, and writes the result into uv.lock with a concrete version. If the package does not exist, or its metadata clashes with a current constraint, the command fails right there and the suggestion never enters the project.

    A clean uv add is the confirmation. The package was real, it was compatible and it now sits in pyproject.toml as a declared dependency and in uv.lock as a pinned version. You can then run uv tree to see exactly where it landed in the graph and what it pulled in with it.

    So the model gives you a name to investigate and the resolver gives you the verdict. You trust the second one. The advisor handed you a name and the resolver gave the verdict. You ran the AI advisor, read the package it suggested for fast CSV parsing and then let uv add resolve and lock it. A clean add is what turned the suggestion into a trusted dependency, recorded in pyproject.toml and pinned in uv.lock.

    You can run the project from inside the datakit/ directory inside its locked environment:

    uv run python main.py
    

    uv run executes the command in the environment described by uv.lock, so the code runs with exactly the resolved dependencies you locked, no manual activation needed.

    Across this lab you took datakit from a loose folder with a legacy requirements.txt to a modern uv project. You scaffolded it with uv init, migrated and managed runtime and development dependencies, locked the environment for reproducibility, read the dependency graph with uv tree, traced a version conflict back to its constraint, and validated an AI-suggested package against the resolver before adopting it.

    A good next step is to apply the same workflow to a larger codebase and compare dependency-management strategies on it. Try migrating a multi-package project to uv, measure how lock and sync times change as the graph grows and weigh the trade-offs in performance, maintainability, and reproducibility against the tools you used before.

About the author

Mateo is currently a full stack web developer working for a company that has clients from Europe and North America. His niche in programming was mostly web oriented, while freelancing, working on small startups and companies that require his services. Go(lang), Elixir, Ruby and C are his favorite languages and also the ones he’s mostly working with other then PHP in day to day work. He has a big passion for learning and teaching what he knows the best. His big interests recently have been the fields of DevOps, Linux, functional programming and machine learning.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight