LLMOpsThe LLMOps lifecycle · MLflow 3 · FastAPI · Docker · AWS/Azure
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
41 small wins to finish your pathNext lesson →

Packaging: setup.py, pyproject and requirements

Packaging turns the folders into an installable, versioned project: a requirements.txt of dependencies, a setup.py that reads it, a pyproject.toml of metadata, and a version string the installed package reports.

Last updated: 15 Oct, 2026 · LLMOps

The The app entry point and its API lesson gave the app a seam for a server to run. Packaging is what lets that server, or a container, install the project by name instead of copying files. The repo carries all three files: a requirements.txt, a setup.py whose get_requirements reads it, and a pyproject.toml. This lesson shows each and runs one check that the installed package reports its version.

The dependency list

Every library the app imports is named here, one per line. This is the list a fresh environment installs, and the list the build reads.

text
fastapi
uvicorn
httpx2
pyyaml
pydantic
python-dotenv
mlflow

A setup.py that reads the requirements

The repo's setup.py keeps one source of truth: it reads requirements.txt and passes the lines to install_requires, dropping blanks and the editable -e . marker, so the list is written once. httpx2 is there because the test client in The app entry point and its API needs it, and mlflow because the tracking lessons import it.

python
from setuptools import find_packages, setup
from typing import List

def get_requirements() -> List[str]:
    # read requirements.txt, drop blanks and the editable "-e ." line
    requirements: List[str] = []
    with open("requirements.txt") as file:
        for line in file.readlines():
            line = line.strip()
            if line and line != "-e .":
                requirements.append(line)
    return requirements

setup(
    name="shop-assistant",
    version="0.1.0",
    packages=find_packages(),
    install_requires=get_requirements(),
)

Project metadata in pyproject.toml

pyproject.toml carries the project's name, version, the Python floor and its dependencies in the modern format. A build tool reads this file to produce the installable artifact.

toml
[project]
name = "shop-assistant"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "fastapi",
    "httpx2",
    "pyyaml",
    "pydantic",
    "mlflow",
]
Watch out. If a project carries both files, pyproject.toml wins: setuptools takes [project].dependencies and silently ignores the install_requires that setup.py computed. Let one file own the list. Keep it in requirements.txt and read it from setup.py, or declare it in pyproject.toml and drop the reader, but do not trust two lists to agree.

The package reports its version

Make the top folder a package by giving it an __init__.py with a version string. Once it is importable, the running app can report exactly which version of itself is deployed.

python
# shop_assistant/__init__.py
__version__ = "0.1.0"
ExampleThe installed package names itself and its version
import shop_assistant

print("package:", shop_assistant.__name__)
print("version:", shop_assistant.__version__)

What packaging gave the project

  • One dependency list, read by the build, so an environment is reproduced from requirements.txt rather than from memory.
  • A version on the package, so a running app and its logs can say which build is live, which matters the moment two versions exist.
  • An installable name, so a container or a server pulls in the project with one install step instead of copying a folder.
Four files across the top, each owning one fact: requirements.txt holds the dependency list read by the build, setup.py reads that list so it is written once, pyproject.toml holds the name, version and the Python it needs, and __init__.py holds __version__ so a running app can say which build it is. All four feed one box, an installable versioned package installed by pip install dot with pinned dependencies, which in turn feeds three outcomes: a container build installs it, a teammate reproduces it, and a running app reports its build.

A folder of scripts against an installed package

A folder of scriptsAn installed package
Set it upCopy files, hope for the bestInstall by name
DependenciesImplicitDeclared in one list
VersionNoneA string the package reports
Imported byA path hackA normal import

Where packaging matters

  • A container build, where the image installs the project and its pinned dependencies in one layer.
  • A reproducible environment, where a teammate or a CI job installs the same versions from the list.
  • A deployed app that reports its build, where the version string ties a running process to a commit.
Watch out. Carrying both a setup.py and a pyproject.toml is fine, but let one own each fact. Keep the dependency list in requirements.txt and read it, rather than writing the same libraries into both files, where the two copies drift apart and an install pulls the wrong set.
Try it yourself
  • Bump __version__ to 0.2.0 and watch the import report the new value.
  • Add a dependency to requirements.txt, call get_requirements() directly and confirm it is in the returned list, then check whether pip install -e . installed it.
  • Install your own project with pip install -e . and import it from another folder.

This is what real progress feels like.