How to build a Python package with uv and install it locally

How to build a Python package with uv and install it locally

This post will walk you through the steps to build a simple Python package and install it locally on your machine or a cloud machine. By the end of this tutorial, you'll have a working package that you can use like any other library in Python, but with your own code - ready to be reused in your next project.

Personally, I've used this to develop data science code locally, package it, and install it on Databricks for execution against cloud-stored data. This way, you can harness cloud compute power while maintaining the code in modular, easily manageable chunks that are simple to test individually. This method promotes cleaner, reusable, and more maintainable code, which is crucial for scaling data science workflows efficiently. Of course, this can also be applied to a software engineering stack.

Step 0: Choose a python package builder - I'm using uv here

There are various ways and tools to build a binary distribution ("package") of your code - as with all software engineering things, it's hard to say which is the right way.

Here on pyopensci.org you can find some pointers on which tools to use.

Two of their choices are poetry and hatch. Personally in this tutorial, I am using uv, which is similar to poetry and uses hatchling as a build-backend by default.

You can find a tutorial on installing uv here: https://docs.astral.sh/uv/getting-started/installation/ - but if you use homebrew, it's simply brew install uv.

Step 1: Create Your Package Directory Structure

To start, we need to create a directory structure for our Python package. If you use a tool like uv, you can create a structure like this with the command

uv init --lib my-package

Here’s what the folder should look like afterwards:

my_package/
|-- src/
    |-- my_package/
      |-- __init__.py
      |-- your_code.py  # Add your module(s) here
      |-- py.typed # empty, indicates to IDEs your code includes type annotations
|-- pyproject.toml
|-- README.md
|-- .python-version

Explanation:

  • README.md: A readme markdown file that explains what your package does. Can technically be empty but should be used for documentation.
  • my-package/ (outer): The root folder for your package.
  • my-package/ (inner): This contains your actual code.
  • __init__.py: An empty file that indicates that this directory should be treated as a Python package.
  • py.typed: An empty file that indicates to IDEs that your packaged code uses type annotations.
  • proproject.toml: A central place to define project metadata, build requirements, and dependencies, especially for packaging and distribution.

Step 2: Write Your Package Code

Create the your_code.py file inside the my_package/ directory (the inner one). You can add any functions or classes that you’d like to include in your package. Here's a simple example:

# my_package/your_code.py
def greet(name: str):
    return f"Hello, {name}!"

You can add more modules (.py files) or subpackages (~folders) following typical Python logic. See this tutorial on the topic from the official Python documentation: https://docs.python.org/3/tutorial/modules.html.

Step 3: Fill the pyproject.toml File with correct metadata

Fill the pyproject.toml file with correct metadata to define your package. After the setup with uv it should look something like this depending on the surrounding environment and project:

[project]
name = "my-package"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
    { name = "Sarah Glasmacher", email = "sarah@sarahglasmacher.com" }
]
requires-python = "==3.11.*"
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

You can however still edit this to suit your needs, for example:

  • name: The name of your package (e.g., 'my-package'), by which it will be installed using tools like pip. You can set this to a different value than your folder name, but it is not recommended since it might introduce confusion (looking at you scikit-learn/sklearn…).
  • version: Version of the package (e.g., '0.1.0').
  • author: Your name or the team's name.
  • description: A brief description of what the package does.

The dependencies should obviously reflect whatever packages are needed to run the code you write within this package. You can

  • add them manually by editing the dependencies list in the pyproject.toml file, either with or without defining a specific version of the package depending on what your code needs
  • OR use commands like uv add <package-name-of-dependency> which will update your pyproject.toml file automatically and install the package in the local environment as well

The last option should be used while writing the code - every time you need a package add it directly via uv and your dependencies should be correct when you're ready to build the package.

Step 4: Build the package

It's time to build your package. This step creates a distribution file that can be installed by others (or by you on another machine).

To build the package, use the following command:

uv build

You want to run this command while in the root directory of your package - meaning in the folder where your pyproject.toml file is.
This command will create a dist/ directory with the built .tar.gz or .whl files. These files are what you need to distribute or install your package.

Step 5: Install the Package Locally

With the package built, you can now install it locally to test it out. Use the following command:

pip install dist/my_package-0.1.0-py3-none-any.whl

The .whl file name will match your package's name and version. If you're developing the package and want to make sure changes are immediately reflected without rebuilding every time, you can also install in editable mode:

pip install -e .

This command installs the package in development mode, meaning changes you make to the source code will be immediately reflected without reinstallation. This is - as the name implies useful to test your packaged code and potentially fix bugs iteratively before shipping the final version.

Step 6: Test the Package

To confirm that your package is correctly installed, open a Python shell or script and try importing it:

from my_package.your_code import greet

print(greet("World"))

If everything is set up correctly, you should see the following output:

Hello, World!

This is also how you would import and use your packaged code on another computer.

Conclusion

Yay, you have successfully created a Python package and installed it locally! At first you might run into some issues with managing all these new configuration files and dependencies (I certainly did…), but I firmly believe that this is a great skill to have long term, especially as you move into larger projects.

This packaging enables you to reuse the code easily across projects and ship it to cloud platforms with less issues. This approach also ideally leads you to think more about modularization - which code forms a reusable unit? How could a huge codebase be separated into individual modules? If you split your code like that, it is easier to test and maintain it.

You can also publish your package to a repository like PyPI, so anyone can install it with pip install. But that's a topic for another post - or simply try your luck with this guide from the Python Packaging Authority recommended by PyPI.