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.
Table of Contents
- Step 0: Choose a python package builder – I’m using uv here
- Step 1: Create Your Package Directory Structure
- Step 2: Write Your Package Code
- Step 3: Fill the pyproject.toml File with correct metadata
- Step 4: Build the package
- Step 5: Install the Package Locally
- Step 6: Test the Package
- Conclusion
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 likepip
. You can set this to a different value than your folder name, but it is not recommended since it might introduce confusion (looking at youscikit-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 thepyproject.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 yourpyproject.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.