10xdev Book: Programming from Zero to Pro with Python

Chapter 12: Modern Package Management: uv, pip, and External Libraries

Chapter Introduction

Python’s Standard Library is fantastic, giving you lots of built-in tools. But the real magic, the thing that makes Python a powerhouse for everything from web development to AI, is its vast ecosystem of external libraries (or packages). These are toolkits created by the global Python community, offering specialized functionality for almost anything you can imagine.

Want to build a web app? Grab Flask or Django. Need to work with data? Pandas and NumPy are your friends. Want to talk to web APIs easily? requests is the go-to.

But using these external tools brings a new challenge: managing dependencies. Different projects often need different versions of the same library, leading to conflicts if you just install everything globally.

This chapter dives into the modern, professional way to handle this using:

  1. Virtual Environments: Creating isolated sandboxes for each project (often handled automatically by uv).
  2. uv: The fast, all-in-one tool we introduced for managing environments and packages via pyproject.toml.
  3. pip (via uv): The underlying installer that uv uses to fetch packages from the Python Package Index (PyPI).
  4. Dependency Locking: Ensuring your builds are reproducible using uv.lock.
  5. uvx: Running Python tools without installing them permanently.

Let’s learn how to harness the power of the Python ecosystem safely and efficiently!


1. Virtual Environments: Your Project Bubbles 🫧

We touched on this during setup, but it’s worth reinforcing: always use a virtual environment for every Python project.

Why? It creates an isolated “bubble” containing a specific Python version and its own set of installed libraries, separate from your system-wide Python and other projects. This prevents version conflicts – Project A can use libraryX v1.0 while Project B uses libraryX v2.0 on the same machine without issues.

Using uv: The great thing about uv is that it often handles this automatically. When you run uv init or uv add in a project folder, it typically creates and manages a .venv folder for you behind the scenes. When you use uv run, it automatically executes your code within that environment.

Manual Activation (If Needed): While uv hides much of this, it’s good to know how to manually activate if your editor or terminal doesn’t pick it up automatically:

  • Windows: \.venv\Scripts\activate
  • macOS/Linux: source .venv/bin/activate You’ll see (.venv) or similar appear in your prompt. Type deactivate to exit.

(Note: The older way used python -m venv venv to create and required manual activation every time. uv streamlines this significantly.)


2. Managing Packages with uv

uv replaces the need for using pip directly for most common tasks and uses the modern pyproject.toml file to manage your project’s dependencies.

  • Adding a Dependency: Installs the package into your virtual environment and adds it to pyproject.toml.
      # Adds the latest version of 'requests'
      uv add requests
      # Adds a specific version
      uv add "requests<3.0.0"
    
  • Adding a Development Dependency: For tools only needed during development (like testing frameworks), use the --dev flag.
      uv add --dev pytest
    

    This adds it to a separate [tool.uv.dev-dependencies] section in pyproject.toml.

  • Installing from pyproject.toml: When you clone a project or update dependencies, uv sync reads the uv.lock file (or pyproject.toml if no lock file exists) and installs the exact required versions, ensuring consistency.
      # Ensure your environment matches the lock file
      uv sync
      # Install dev dependencies too
      uv sync --dev
    
  • Removing a Dependency: Removes the package from the environment and pyproject.toml.
      uv remove requests
    
  • Listing Installed Packages: Use the underlying pip command via uv.
      uv pip list
      uv pip freeze # Shows exact versions for requirements files
      uv pip tree   # Shows dependency tree
    

pyproject.toml and uv.lock:

  • pyproject.toml: Defines your direct dependencies and project settings. You edit this.
  • uv.lock: Automatically generated/updated by uv. It locks down the exact versions of all packages (direct and indirect dependencies) for reproducible builds. You typically commit this file to version control.

(Contrast: Older projects used requirements.txt, often generated by pip freeze. uv can read these but prefers pyproject.toml for better structure and dependency resolution.)


3. Example: Talking to Web APIs with requests

Let’s use the requests library (make sure you’ve run uv add requests) to fetch data from a public API. An API (Application Programming Interface) is like a restaurant menu and waiter – it defines how your program can request specific information from a web server. We’ll use DummyJSON again.

import requests
import sys

# API endpoint for a specific product
api_url: str = "[https://dummyjson.com/products/1](https://dummyjson.com/products/1)"

try:
    print(f"Fetching data from {api_url}...")
    # Send an HTTP GET request
    response = requests.get(api_url, timeout=10) # Added timeout

    # Check for HTTP errors (4xx or 5xx status codes)
    response.raise_for_status()
    print("Request successful!")

    # Parse the JSON response into a Python dictionary
    data: dict = response.json()

    # Safely extract data using .get()
    title: str | None = data.get("title")
    price: float | None = data.get("price")

    if title and price is not None:
        print("\nProduct Details:")
        print(f"  Title: {title}")
        print(f"  Price: ${price:.2f}")
    else:
        print("Title or price not found in response.")

except requests.exceptions.Timeout:
    print(f"Error: Request timed out contacting {api_url}")
    sys.exit(1)
except requests.exceptions.HTTPError as e:
    print(f"Error: HTTP Error occurred: {e.response.status_code} {e.response.reason}")
    sys.exit(1)
except requests.exceptions.RequestException as e:
    # Catch any other requests-related errors (DNS, connection, etc.)
    print(f"Error: Could not connect to API: {e}")
    sys.exit(1)
except requests.exceptions.JSONDecodeError:
    print("Error: Could not decode JSON response from server.")
    sys.exit(1)

This shows the basic pattern: make a request, check for errors (raise_for_status), parse the JSON, and extract the data you need.


4. Running Tools On-the-Fly with uvx

What if you want to quickly run a Python command-line tool (like a code formatter black or linter ruff) without permanently installing it in your project or globally? uvx is the answer.

# Format a file using the latest 'black' formatter
uvx black my_script.py

# Run a specific version of the 'ruff' linter on your project
uvx --package ruff==0.1.9 ruff check .

# Start a temporary Jupyter Lab instance
uvx jupyter lab

Behind the scenes, uvx creates a temporary, cached environment, installs the tool there, runs it, and then cleans up. It’s incredibly handy for one-off tasks or trying out tools without cluttering your main environment.


5. Chapter Summary

  • Always use virtual environments to isolate project dependencies; uv simplifies their creation and management.
  • Use uv add <package> to install libraries and update pyproject.toml.
  • pyproject.toml defines direct dependencies; uv.lock ensures reproducible builds.
  • Use uv sync to install dependencies based on the lock file.
  • External libraries like requests dramatically extend Python’s capabilities (e.g., for working with APIs).
  • Use uvx <command> to run Python tools without installing them in your project.

You can now manage dependencies like a pro and leverage the vast Python ecosystem! But as your projects use more libraries and grow in complexity, how you structure your own code becomes even more critical. Let’s learn how to organize your project into logical modules and packages.