- Lab
-
Libraries: If you want this lab, consider one of these libraries.
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 Info
Table of Contents
-
Challenge
Introduction
Python dependency management has changed. For years the routine was
pip, a hand-editedrequirements.txtand 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.uvreplaces 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. Learninguvis 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 legacyrequirements.txtand no real packaging. Your job across this lab is to rebuild it into a modern, reproducibleuvproject.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.txtis the old-style dependency list the previous team left behind. You migrate it into the project, but you do not edit it.legacy/README.mddescribes the oldpipandvenvworkflow, for comparison. You do not edit it.tools/ai_advisor.pyis 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.csvis a small sample file the utility reads, so the project has something to run against. You do not edit it..envholds aLAB_API_KEY=line. You paste your key into this one line before running the AI advisor.
There is deliberately no
pyproject.toml, nouv.lock, and no.venvyet, 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 thedatakit-workspacedirectory.uvis already installed and on thePATH. TheanthropicSDK andpython-dotenvare already installed withpip3so 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
.envfile as the value ofLAB_API_KEY.You use the Terminal to run every
uvcommand in this lab.Click on the Next step arrow to get started.
-
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 withpip install, then freeze the result into arequirements.txtby hand. Nothing records the exact versions that resolved, so the samerequirements.txtcan 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.uvcollapses that whole loop into one tool. A single command scaffolds a project, and from then onuvmanages 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 bothsetup.pyandrequirements.txt.You start a new
uvproject by runninguv initwith a project name. For an unrelated example:uv init weather-cliThis creates a
weather-clidirectory containing a ready-to-usepyproject.toml, so the project is a real package from the very first command. Runninguv init datakitcreated adatakit/subfolder, and that folder is now the home of the project. Every lateruvcommand in this lab runs from inside it, so change into it once withcd datakitbefore you continue.Open
datakit/pyproject.tomland 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
nameandversionidentify the package.requires-pythondeclares which interpreter versions the project supports. Thedependenciesarray is the list of packages the project needs at runtime, and it starts empty. This single standard file does the job thatsetup.pyandrequirements.txtused to split between them. Thedatakitfolder is now a real uv package. A singleuv initgave it a standardpyproject.tomland the legacyrequirements.txtbecame managed entries in the project'sdependenciesarray. 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. -
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.
uvgives you one command for each move.uv addrecords a runtime dependency in thedependenciesarray.uv add --devrecords a development-only dependency in a separate dependency group, so it never ships as part of the runtime requirements.uv removedeletes a dependency you no longer want and updates the project for you.For example, if you had a
weather-cliproject 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 requestsEach command edits
pyproject.tomlfor 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.typersits in the runtimedependenciesarray because the utility calls it when it runs,pytestlives in a separate development dependency group because it is only used while building the project, andrequestsis gone now thathttpxcovers every HTTP call. The dependency set indatakit/pyproject.tomlreflects what the project actually requires, nothing more. -
Challenge
Lock for reproducible environments
A
pyproject.tomldeclares 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 samepyproject.tomlweeks 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.uvsplits these two roles across two files.pyproject.tomlholds your intent as version specifiers, anduv.lockholds the resolved result thatuv lockcomputes from that intent. A version specifier controls how much room the resolver has.==2.1.0pins one exact version,>=2.1allows that version or anything newer and~=2.1allows patch and minor updates inside the2.xline while holding the major version steady.For example, a
weather-cliproject might loosen one dependency to a known-good floor while keeping the others open:dependencies = [ "pendulum>=3.0", "click", ]Here
pendulummust resolve to3.0or newer whileclickis free to resolve to whatever the resolver picks. Runninguv lockthen turns those specifiers into a fully pinneduv.lock. The project now carries both halves of a reproducible setup.datakit/pyproject.tomlstates the intent with a pinnedhttpx>=0.27floor, anddatakit/uv.lockrecords the exact versions the resolver chose for every direct and transitive package. Runninguv syncturned that lock file into a real.venvon disk. These two files are what teammates commit and share, because anyone who checks them out and runsuv syncrebuilds the identical environment. -
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
dependenciesarray inpyproject.tomllists 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.
uvprints this graph withuv tree.For example, a small
notes-cliproject that depends only onrequestsmight 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.3Here
requestsis the only direct dependency and the four packages nested under it are transitive. They arrived becauserequestsneeds 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
thumbnailerrequirespillow>=10, the two constraints collide. The resolver cannot pick a singlepillowthat is both exactly9.0and at least10, souv lockstops 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
datakitproject already carriestyper, andtyperneeds a recentrich. That makes it a safe place to watch a real conflict happen. Pinningrichto an old release that predates whattyperrequires forcesuv lockto 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 treeshowed how each direct dependency branches into the transitive packages underneath it, so a package you never named has a visible reason for being there. Pinningrichto the incompatiblerich==10.0madeuv lockfail, and the resolver error pointed straight at the constraint that could not be satisfied. Relaxing that pin torich>=13and re-runninguv locklet resolution succeed again with arichthat both your project andtyperaccept. -
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:
zoomrequestsThat 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.pyscript 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
.envfile as the value ofLAB_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 runuv addwith the suggested name,uvlooks the package up on the index, resolves its requirements against everything the project already declares, and writes the result intouv.lockwith 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 addis the confirmation. The package was real, it was compatible and it now sits inpyproject.tomlas a declared dependency and inuv.lockas a pinned version. You can then runuv treeto 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 addresolve and lock it. A clean add is what turned the suggestion into a trusted dependency, recorded inpyproject.tomland pinned inuv.lock.You can run the project from inside the
datakit/directory inside its locked environment:uv run python main.pyuv runexecutes the command in the environment described byuv.lock, so the code runs with exactly the resolved dependencies you locked, no manual activation needed.Across this lab you took
datakitfrom a loose folder with a legacyrequirements.txtto a modern uv project. You scaffolded it withuv init, migrated and managed runtime and development dependencies, locked the environment for reproducibility, read the dependency graph withuv 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
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.