Skip to content
  • Home
  • How To
  • M0
  • M1
  • Agents
  • M2
  • M3
  • M4
  • M5
  • Module 6 - MLOps
  • Progress
  • Contact
  • Pricing
  • Sign in
  • Future
  1. Module 6M6
  2. ›
    S1Project Discipline and Testing
  3. ›
    Lesson 1 of 3From Script to Project
  4. ›
    S1›Lesson 1 of 3
  1. Module 6
  2. ›
  3. Section 1
  4. ›
  5. Lesson 1

From Script to Project

FREE·~1.5 h·2 labs·4 questions·1 assignment


Context

A project is a folder that carries its own environment: a pyproject.toml that names the libraries the project chooses, at exact versions, a lock file that records every package those libraries pull in, and the code itself as an installed package of importable modules. uv reads those files and builds the same environment from them on any machine, with one command.

Every model that leaves your laptop travels this way. A teammate who clones the repository, the automated pipeline that tests every push, and the container that later serves the price all start from those files and nothing else. The training script becomes a module you run by name, and its pieces, the loader, the split and the pipeline, become functions that tests and a service import instead of copying.

Without it, Lesson 3.2.3's train_price.py runs in exactly one place, (bootcamp), the shared environment Modules 2 to 5 filled. Deactivate it and the same script stops on its first line with ModuleNotFoundError: No module named 'joblib', and nothing in the folder says which libraries it needs, at which versions, or which of the many installed in (bootcamp) its printed score of 56,562 depended on.


Goal

  • Build a uv project around Lesson 3.2.3's price script with uv init, and explain what each of pyproject.toml, .python-version and .venv decides.
  • Pin pandas 3.0.5, scikit-learn 1.9.1 and NumPy 2.5.2 with uv add, and read uv.lock, uv tree and [tool.uv] exclude-newer to say where every installed package came from and why it will resolve the same tomorrow.
  • Rebuild .venv from the lock with uv sync, check the lock with uv lock --check, and distinguish what uv run repairs from what only uv sync removes.
  • Refactor the script into a src layout package of data.py, features.py and train.py, and run it with uv run python -m listings.train to the same score and the same saved bytes.
  • Diagnose the three failures of a package run from the wrong place, from a subfolder, from outside the project and on the system python3, and fix each one.
  • Decide what the repository keeps and what .gitignore leaves out, and commit the project so a clone rebuilds and retrains it.

Prerequisites

None of this is required, and the lesson stands on its own. Both are here if you want to watch the uv workflow end to end, or read uv's own walk through a project's files, before you build one.

  • YouTube - "Python Tutorial: UV - A Faster, All-in-One Package Manager to Replace Pip and Venv" by Corey Schafer. Initializing a project, adding dependencies through pyproject.toml and a lock file, the environment uv manages for you, and running scripts, set beside the pip and venv way of doing the same.
  • uv documentation - Working on projects, the sections "Project structure" and "Running commands". What pyproject.toml, .python-version, .venv and uv.lock each hold, and the check uv run makes against the lock before every command.

Table of Contents

  • The Script, Restated
  • A uv Project
  • Exact Pins With uv add
  • The Lock and uv sync
  • The src Layout and python -m
  • The Folder a Command Runs From
  • What git Keeps

Theory

Every command runs in ~/dev/listings-service with (bootcamp) deactivated, as Module 6's setup page says, and a transcript shows the folder in its prompt, goku@machine:~/dev/listings-service$. This lesson downloads pandas 3.0.5, scikit-learn 1.9.1 and NumPy 2.5.2, with the seven libraries they need on WSL, Linux and macOS, into the project's own environment with one uv add line in the third subtitle, and nothing goes into (bootcamp). uv prints its own milliseconds on every command, so your timings differ from the ones quoted, and on a machine whose uv cache does not hold these libraries yet, uv also prints a Downloading line for each large one.

The Script, Restated

What. train_price.py is the solution to Lesson 3.2.3's Assignment: it reads listings.csv, splits the nine feature columns from price with test_size=0.2 and random_state=42, fits a pipeline whose numeric group fills each gap with the training median, keeps an indicator of where the gap was and scales, one-hot encodes the three nominal columns and orders condition from poor to excellent, then prints the test MAE and R² and saves the fitted pipeline with joblib. Why. It is the model Module 6 carries from a script to a monitored service, and it is restated here in full so that if you start at this module you need the course's listings.csv and nothing else from an earlier one. When. Once, now, as the first file of the project folder.

The project starts as a folder holding the data. If listings.csv, the course's download of 3,000 listings in 13 columns, is not in it yet, download it from data/m3/ in the course's GitHub repository and copy it from where your browser saved it:

mkdir -p ~/dev/listings-service
cd ~/dev/listings-service
# WSL, from the Windows Downloads folder
cp /mnt/c/Users/goku/Downloads/listings.csv .
# Linux and macOS
cp ~/Downloads/listings.csv .

Save the script beside it as train_price.py, exactly as Lesson 3.2.3 wrote it, with the two lines it printed there written above the two print calls:

import joblib
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler

listings = pd.read_csv("listings.csv")
numeric = ["area_sqm", "year_built", "distance_km", "rooms", "bathrooms"]
nominal = ["neighborhood", "property_type", "energy_rating"]
features = numeric + nominal + ["condition"]
X_train, X_test, y_train, y_test = train_test_split(
    listings[features], listings["price"], test_size=0.2, random_state=42
)

numeric_steps = Pipeline([
    ("impute", SimpleImputer(strategy="median", add_indicator=True)),
    ("scale", StandardScaler()),
])
levels = ["poor", "fair", "good", "excellent"]
prep = ColumnTransformer([
    ("num", numeric_steps, numeric),
    ("cat", OneHotEncoder(sparse_output=False), nominal),
    ("cond", OrdinalEncoder(categories=[levels]), ["condition"]),
])
pipe = Pipeline([("prep", prep), ("model", LinearRegression())])
pipe.fit(X_train, y_train)

predicted = pipe.predict(X_test)
mae = mean_absolute_error(y_test, predicted)
# 56,562 0.766
print(f"{mae:,.0f}", round(r2_score(y_test, predicted), 3))
# ['price_pipeline.joblib']
print(joblib.dump(pipe, "price_pipeline.joblib"))

With (bootcamp) deactivated, python3 is the system's Python, and it has none of the libraries the script imports:

goku@machine:~/dev/listings-service$ python3 train_price.py
Traceback (most recent call last):
  File "/home/goku/dev/listings-service/train_price.py", line 1, in <module>
    import joblib
ModuleNotFoundError: No module named 'joblib'

The script names its libraries in its imports and their versions nowhere. In Module 3 it ran because (bootcamp) happened to hold scikit-learn 1.9.1, which brought joblib, and pandas 3.0.5 from Module 2, beside every other library Modules 2 to 5 installed there. The error stops at line 1 because joblib is the first import, so it says nothing about the eight import lines below it, and nothing in the folder records that this score needs pandas 3.0.5 rather than whatever pandas a machine has.

DON'T:

  • DON'T reactivate (bootcamp) to make the script run. It would print the same score on your machine, from an environment that holds every library of Modules 2 to 5, and the folder would still record none of them for anyone else.

A uv Project

What. uv init --package --name listings --python 3.12.8 turns the current folder into a uv project. It writes pyproject.toml, the project's description and the list of what it depends on, .python-version, which holds 3.12.8, a README.md, the package folder src/listings/ with its __init__.py, and a .gitignore, and it runs git init when the folder is not inside a repository already. --package asks for an installable package under src/ with a build backend (the tool that turns the folder into something installable), --name listings names it, and --python 3.12.8 writes the interpreter into .python-version and requires-python. Why. A project's environment then lives in the project. uv run reads pyproject.toml and .python-version, builds .venv inside the folder on Python 3.12.8, and runs the command in it, so the folder alone says what the code needs, with no environment to activate and none shared with another project. When. Once per project, in its folder, before its first dependency.

In ~/dev/listings-service:

uv init --package --name listings --python 3.12.8
Initialized project `listings`

--name is needed because the folder's name is listings-service, and without it uv names the project listings-service and the package listings_service, the import name with the hyphen turned into an underscore. uv 0.13.0 writes the src layout without --package too, and the flag keeps the command's meaning from depending on that default. A second uv init in the same folder changes nothing and stops with exit code 2 and an error that the project is already initialized, naming the folder and the pyproject.toml that is already there. pyproject.toml as uv init wrote it:

[project]
name = "listings"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
    { name = "goku", email = "goku@gmail.com" }
]
requires-python = ">=3.12.8"
dependencies = []

[project.scripts]
listings = "listings:main"

[build-system]
requires = ["uv_build>=0.13.0,<0.14.0"]
build-backend = "uv_build"

[project] is the package's identity: authors comes from your git config name and email, which Module 0 set, requires-python refuses any Python older than 3.12.8, and dependencies is empty. [project.scripts] would install a terminal command named listings that calls a main() function uv init wrote into src/listings/__init__.py, a placeholder that prints Hello from listings!. This project runs its training as a module instead, in the fifth subtitle, so the table and the placeholder both go. [build-system] names uv_build, uv's own build backend, at the range that belongs to uv 0.13.0, the uv this lesson ran.

Edit pyproject.toml to read:

[project]
name = "listings"
version = "0.1.0"
description = "Prices a property listing, from a training script to a monitored service."
readme = "README.md"
authors = [
    { name = "goku", email = "goku@gmail.com" }
]
requires-python = ">=3.12.8"
dependencies = []

[build-system]
requires = ["uv_build>=0.13.0,<0.14.0"]
build-backend = "uv_build"

[tool.uv]
exclude-newer = "2026-10-20T00:00:00Z"

[tool.uv] is uv's own settings table, and exclude-newer goes in before the first dependency, because it decides what that dependency resolves to: the fourth subtitle reads it. Replace the placeholder in src/listings/__init__.py with the package's one-line docstring:

"""Price a property listing: data, pipeline, training and, later, the service."""

README.md is empty, and readme = "README.md" makes it the package's description, so give it two lines:

# listings-service

Prices a property listing from `data/listings.csv`, grown from a training script into a monitored service.

Run the script inside the project's environment:

goku@machine:~/dev/listings-service$ uv run python train_price.py
Using CPython 3.12.8
Creating virtual environment at: .venv
   Building listings @ file:///home/goku/dev/listings-service
      Built listings @ file:///home/goku/dev/listings-service
Installed 1 package in 0.62ms
Traceback (most recent call last):
  File "/home/goku/dev/listings-service/train_price.py", line 1, in <module>
    import joblib
ModuleNotFoundError: No module named 'joblib'

uv run found pyproject.toml, read 3.12.8 from .python-version, created .venv in the folder, wrote uv.lock, built the listings package from src/ and installed it, the one package, then ran python from .venv. The error is the same as the system Python's, and now it is the right one: the project declares no dependencies, so its environment holds none. The fix is not an install by hand but a line in dependencies, the next subtitle's.

DON'T:

  • DON'T leave --name listings off. The folder's name becomes the project's, and the package you import becomes listings_service.
  • DON'T read Installed 1 package as a working environment. The one package is the project itself, and its dependencies = [] gave .venv nothing else to hold.

Exact Pins With uv add

What. uv add pandas==3.0.5 scikit-learn==1.9.1 numpy==2.5.2 writes the three requirements into dependencies, resolves them with everything they need in turn, writes the result to uv.lock and installs it into .venv, all in one command. == pins each one to a single release. Why. These three are the versions every figure in this bootcamp was printed with, the same pins as Module 3's. pandas and scikit-learn do not pin NumPy for you: they accept a range, and a fresh lock of this project made without the numpy line picked numpy 2.5.4, uploaded on 2026-10-10, inside the cutoff. A library is pinned directly when the project chooses its version: the two whose functions the pipeline is built from, and NumPy, whose version would otherwise move with whatever the other two accept. When. For every library whose version the project's numbers depend on, at the moment the code first needs it.

uv add pandas==3.0.5 scikit-learn==1.9.1 numpy==2.5.2
Resolved 12 packages in 31ms
   Building listings @ file:///home/goku/dev/listings-service
      Built listings @ file:///home/goku/dev/listings-service
Prepared 1 package in 7ms
Uninstalled 1 package in 0.45ms
Installed 11 packages in 24ms
 + cloudpickle==3.1.2
 + joblib==1.6.0
 ~ listings==0.1.0 (from file:///home/goku/dev/listings-service)
 + narwhals==2.27.1
 + numpy==2.5.2
 + pandas==3.0.5
 + python-dateutil==2.9.0.post0
 + scikit-learn==1.9.1
 + scipy==1.18.1
 + six==1.17.0
 + threadpoolctl==3.7.0

Three names went in and ten libraries came out, with one ~ line. The + lines are new installs: pandas brings python-dateutil (parses dates), which brings six (a compatibility layer for old Python code), and scikit-learn brings joblib, narwhals, SciPy and threadpoolctl, with cloudpickle under joblib. The ~ line is uv rebuilding the project's own package, uninstalled and installed again because pyproject.toml changed, so Installed 11 packages counts it. Resolved 12 packages counts one more than the installs, and the fourth subtitle finds which. dependencies now reads:

dependencies = [
    "numpy==2.5.2",
    "pandas==3.0.5",
    "scikit-learn==1.9.1",
]

uv sorted the three by name. The same script, run the same way:

goku@machine:~/dev/listings-service$ uv run python train_price.py
56,562 0.766
['price_pipeline.joblib']

Lesson 3.2.3's two lines, to the digit, from an environment that holds those three libraries, what they need, and nothing more. uv run printed none of its own lines this time, because the lock and .venv already matched pyproject.toml.

DON'T:

  • DON'T leave the == off. uv add without a version writes only a lower bound, the version it found that day, as a clone of this project wrote "httpx>=0.28.1" for a bare uv add httpx, and any later release above it satisfies the line.
  • DON'T drop the NumPy pin because pandas and scikit-learn install NumPy anyway. They accept a range, and a fresh lock without the pin resolved 2.5.4.

The Lock and uv sync

What. uv.lock is the resolution written down: every package, the project included, at one exact version, with the address of every file it can be installed from and each file's SHA-256 hash, a fingerprint uv checks before it installs a download, for every platform the project can run on, not only yours. [tool.uv] exclude-newer sets a cutoff, so a resolution considers only files uploaded to the package index before 2026-10-20T00:00:00Z. uv sync makes .venv match the lock exactly, and uv run makes the same check before every command. Why. pyproject.toml says what the project asks for, three libraries, and the lock says what it got, twelve packages. With the lock committed, every machine installs the same twelve from the same files, and with the cutoff, a resolution made from nothing on any later day picks what this one picked, instead of whatever was uploaded since. When. The lock is written by uv add, uv lock, uv sync and uv run, and read by every one of them. You commit it, read it, and never edit it by hand.

The lock opens with the Python range it was resolved for, the six combinations of platform and Python version it resolved separately, and the cutoff, then lists each package. The project's own entry records what pyproject.toml asked for:

version = 1
revision = 5
requires-python = ">=3.12.8"
resolution-markers = [
    "python_full_version >= '3.14' and sys_platform == 'win32'",
    "python_full_version >= '3.14' and sys_platform == 'emscripten'",
    "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
    "python_full_version < '3.14' and sys_platform == 'win32'",
    "python_full_version < '3.14' and sys_platform == 'emscripten'",
    "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
]

[options]
exclude-newer = "2026-10-20T00:00:00Z"
[[package]]
name = "listings"
version = "0.1.0"
source = { editable = "." }
dependencies = [
    { name = "numpy" },
    { name = "pandas" },
    { name = "scikit-learn" },
]

[package.metadata]
requires-dist = [
    { name = "numpy", specifier = "==2.5.2" },
    { name = "pandas", specifier = "==3.0.5" },
    { name = "scikit-learn", specifier = "==1.9.1" },
]

source = { editable = "." } says the project is installed from this folder as an editable install, which the fifth subtitle opens. uv tree draws the lock as the graph it is, each package under the one that needs it:

goku@machine:~/dev/listings-service$ uv tree
Resolved 12 packages in 1ms
listings v0.1.0
├── numpy v2.5.2
├── pandas v3.0.5
│   ├── numpy v2.5.2
│   └── python-dateutil v2.9.0.post0
│       └── six v1.17.0
└── scikit-learn v1.9.1
    ├── joblib v1.6.0
    │   └── cloudpickle v3.1.2
    ├── narwhals v2.27.1
    ├── numpy v2.5.2
    ├── scipy v1.18.1
    │   └── numpy v2.5.2
    └── threadpoolctl v3.7.0

The tree holds eleven packages and the lock twelve. The twelfth is pandas's other dependency, which the lock records with a condition:

    { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },

tzdata (a copy of the world's time-zone database) is needed only on Windows and in the browser, so uv locked it, tzdata 2026.5, and left it out of .venv on WSL, which is Linux, where uv tree draws the tree for the machine it runs on. A teammate on native Windows gets it from the same lock. That is the twelve resolved, eleven installed, the project counted in both.

.venv is a product of the lock, and uv sync rebuilds it from nothing:

goku@machine:~/dev/listings-service$ rm -rf .venv && uv sync
Using CPython 3.12.8
Creating virtual environment at: .venv
Resolved 12 packages in 1ms
Installed 11 packages in 59ms
 + cloudpickle==3.1.2
 + joblib==1.6.0
 + listings==0.1.0 (from file:///home/goku/dev/listings-service)
 + narwhals==2.27.1
 + numpy==2.5.2
 + pandas==3.0.5
 + python-dateutil==2.9.0.post0
 + scikit-learn==1.9.1
 + scipy==1.18.1
 + six==1.17.0
 + threadpoolctl==3.7.0

Resolved 12 packages in 1ms is uv confirming the lock still answers pyproject.toml, not resolving again, and the installs came from uv's cache, so the environment was back in a fraction of a second. uv lock --check asks the first half of that question alone and changes nothing: here it prints Resolved 12 packages in 1ms and exits with 0, and after an edit to pyproject.toml that no lock has followed, it prints error: The lockfile at `uv.lock` needs to be updated, but `--check` was provided. and a hint to run uv lock, and exits with 1.

The cutoff decides what a resolution picks. Without it, a lock made from nothing takes the newest files the index holds on the day it runs, so the same pyproject.toml locks differently next month, in the packages nobody pinned. With it, the newest each package could be is whatever was uploaded before 2026-10-20, which no later upload can change. A lock that already exists holds its versions by itself: uv lock and uv add keep every locked version that still satisfies pyproject.toml, so the cutoff matters when a lock is made from nothing or a new package arrives.

uv run and uv sync keep .venv true to the lock in two different strengths. In a clone of this project, uv pip install scikit-learn==1.9.0 replaced the locked 1.9.1 inside .venv, and the next uv run printed Uninstalled 1 package and Installed 1 package and imported 1.9.1 again. uv pip install rich==14.0.0 added rich (a library for coloured terminal output) and three libraries it needs, and the next uv run left all four in place, because the lock does not name them. uv sync removed them:

Resolved 12 packages in 1ms
Uninstalled 4 packages in 4ms
 - markdown-it-py==4.2.0
 - mdurl==0.1.2
 - pygments==2.21.0
 - rich==14.0.0

uv run repairs every package the lock names and leaves the rest, and uv sync makes .venv hold the lock and nothing else.

DON'T:

  • DON'T install into the project's environment with uv pip install. Nothing records it, the next uv sync removed rich and its three libraries, and a package the lock names went straight back to its locked version on the next uv run.
  • DON'T edit uv.lock by hand, and DON'T delete it to fix an environment. rm -rf .venv && uv sync rebuilds the environment from it, and pyproject.toml followed by uv lock is how its contents change.
  • DON'T read the exclude-newer date as a pin. It caps what any package can resolve to, and the three == lines still decide pandas, scikit-learn and NumPy exactly.

The src Layout and python -m

What. The script becomes three modules of the listings package under src/listings/: data.py holds the file path, the column lists and the split, features.py builds the unfitted pipeline, and train.py fits, scores and saves it. uv run python -m listings.train runs train.py by its import name: -m finds the module the way import listings.train would, sets its __name__ to "__main__" so its main guard fires, and puts the folder you started in first on sys.path, where launching a file by its path puts the file's own folder. Why. Each later piece of the project needs a different part of the script: a test checks the split, a tracked run compares pipelines, a service loads what training saved. As functions in modules, each part is imported where it is needed and written once. The src folder keeps the package off the import path until it is installed, so the code that runs is always the installed package and never a copy that happens to sit in the folder you are in. When. As soon as a second program needs any part of the script, which in this module is the next lesson.

Move the data into a data/ folder and delete the script and what it saved, which the package replaces:

mkdir data
mv listings.csv data/
rm train_price.py price_pipeline.joblib

src/listings/data.py holds every fact about the table, so no other module types a column name:

"""Load the listings and split them the one way every lesson uses."""

import pandas as pd
from sklearn.model_selection import train_test_split

DATA_PATH = "data/listings.csv"
NUMERIC = ["area_sqm", "year_built", "distance_km", "rooms", "bathrooms"]
NOMINAL = ["neighborhood", "property_type", "energy_rating"]
LEVELS = ["poor", "fair", "good", "excellent"]
FEATURES = NUMERIC + NOMINAL + ["condition"]
TARGET = "price"


def load(path=DATA_PATH):
    return pd.read_csv(path)


def split(listings):
    return train_test_split(
        listings[FEATURES], listings[TARGET], test_size=0.2, random_state=42
    )

src/listings/features.py builds the pipeline from those lists and fits nothing, so the caller decides which rows fit sees:

"""The preprocessing and the model, as one unfitted pipeline."""

from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler

from listings.data import LEVELS, NOMINAL, NUMERIC


def build_pipeline():
    numeric_steps = Pipeline([
        ("impute", SimpleImputer(strategy="median", add_indicator=True)),
        ("scale", StandardScaler()),
    ])
    prep = ColumnTransformer([
        ("num", numeric_steps, NUMERIC),
        ("cat", OneHotEncoder(sparse_output=False), NOMINAL),
        ("cond", OrdinalEncoder(categories=[LEVELS]), ["condition"]),
    ])
    return Pipeline([("prep", prep), ("model", LinearRegression())])

from listings.data import ... is an absolute import, by the package's name, so it means the same module whichever program imports features.py. Save the third module as src/listings/train.py, and run it from the project folder with uv run python -m listings.train:

"""Fit the price pipeline on the training rows, score it, save it."""

from pathlib import Path

import joblib
from sklearn.metrics import mean_absolute_error, r2_score

from listings.data import load, split
from listings.features import build_pipeline

MODEL_PATH = Path("models/price_pipeline.joblib")


def evaluate(pipe, X, y):
    predicted = pipe.predict(X)
    return {
        "mae": mean_absolute_error(y, predicted),
        "r2": r2_score(y, predicted),
    }


def main():
    X_train, X_test, y_train, y_test = split(load())
    pipe = build_pipeline().fit(X_train, y_train)
    scores = evaluate(pipe, X_test, y_test)
    print(f"{scores['mae']:,.0f}", round(scores["r2"], 3))
    MODEL_PATH.parent.mkdir(exist_ok=True)
    print(joblib.dump(pipe, MODEL_PATH))


if __name__ == "__main__":
    main()
56,562 0.766
['models/price_pipeline.joblib']

The score is the script's, and so is the file: models/price_pipeline.joblib is 5,554 bytes, and cmp (which compares two files byte by byte and prints nothing when they match) found it identical to the price_pipeline.joblib that train_price.py saved in a clone of the project, because the same rows went through the same steps on the same libraries. evaluate returns the two scores instead of printing them, so a later caller can compare them, and main holds what the script did at the top level, so importing listings.train runs nothing. mkdir(exist_ok=True) creates models/ on the first run and does nothing on the next. joblib.dump returned the path as written, relative, which matters in the next subtitle.

The import of listings works from .venv because the project is installed there as an editable install: instead of a copy of the code, .venv holds a one-line file that adds src/ to the import path:

goku@machine:~/dev/listings-service$ cat .venv/lib/python3.12/site-packages/listings.pth
/home/goku/dev/listings-service/src

Python reads every .pth file in site-packages when it starts and adds each line to sys.path, so import listings finds src/listings/, the folder you edit, and an edit to train.py reaches the next run with nothing reinstalled. uv run python src/listings/train.py also runs, through the same .pth line, because its imports name the package. -m is the form this module uses throughout, because it names what runs the way an import names it, independent of where the file sits.

DON'T:

  • DON'T import a sibling module by its bare name, from data import load. Inside a package every import starts at the package, from listings.data import load, so it means the same module whether train.py was launched, imported by a test or run with -m.
  • DON'T leave main()'s work at the top level of train.py. Under the main guard, a test or a service can import evaluate without training a model on import.

The Folder a Command Runs From

What. uv run finds its project by searching upward from the folder you are in for a pyproject.toml, so it uses the project's .venv from any folder inside it. A relative path, data/listings.csv or models/price_pipeline.joblib, is resolved against the same folder, the working directory the process started in, and not against the project or the file that holds the path. Imports and files are found by two different rules. Why. That is where three of this module's commonest failures come from: a command started from a subfolder imports everything and finds no data, a command started outside the project finds no project, and the system python3 started from the right folder finds no package. When. Before any command of this module runs: every one starts from the project folder, ~/dev/listings-service, or names it.

From src/, a folder inside the project, the import succeeds and the read fails. The traceback runs through train.py, line 23, and data.py, line 15, into pandas's own files under .venv/lib/python3.12/site-packages/, and ends:

goku@machine:~/dev/listings-service/src$ uv run python -m listings.train
FileNotFoundError: [Errno 2] No such file or directory: 'data/listings.csv'

uv run walked up from src/ to pyproject.toml, so .venv, the libraries and listings were all found, and pd.read_csv("data/listings.csv") looked for src/data/listings.csv. From ~/dev, outside the project, there is no pyproject.toml above the folder to find, so uv run starts a Python 3.12 of its own, with no project and no libraries, which stops with No module named 'listings'. The system Python started from the right folder fails on the import instead:

goku@machine:~/dev/listings-service$ python3 -m listings.train
/usr/bin/python3: Error while finding module specification for 'listings.train' (ModuleNotFoundError: No module named 'listings')

python3 without uv run is not the project's interpreter. -m put the project folder on sys.path, and the package is not there: it is in src/, and only .venv's .pth file points at it. This is the src layout doing its job. A copy of the same package laid out flat, listings/ directly in the project folder, made the same command find the package in the current folder and fail one step later, ModuleNotFoundError: No module named 'joblib' from train.py, line 5, so the folder's code ran on an interpreter that has none of its libraries. With src, the only code that imports is the installed package in the environment that holds its libraries.

Two fixes keep the paths as they are. Run from the project folder, the habit this module keeps: cd ~/dev/listings-service and then uv run. Or name the folder: uv run --directory ~/dev/listings-service python -m listings.train changes into that folder before it does anything, so from ~/dev it prints 56,562 0.766 and ['models/price_pipeline.joblib']. Its neighbor --project only chooses which project's environment to use and leaves the working directory where it was, so from src/ it fails exactly as plain uv run does.

DON'T:

  • DON'T read the FileNotFoundError from src/ as a broken install. The imports all worked, and the path data/listings.csv was looked up in src/.
  • DON'T use --project to run from elsewhere. It picks the environment and keeps the working directory, so the relative paths still miss, and --directory is the flag that moves the process.
  • DON'T run the package with a bare python3. It is the system's interpreter, not .venv's, and uv run python is the one that has the package and its libraries.

What git Keeps

What. The repository keeps what nobody can regenerate: pyproject.toml, uv.lock, .python-version, README.md, src/ and data/listings.csv. .gitignore keeps out what a command rebuilds: .venv, rebuilt by uv sync, __pycache__/, rebuilt by Python on import, and models/, rebuilt by uv run python -m listings.train. uv init wrote the first two into .gitignore, and the third is yours to add. Why. A clone then rebuilds the environment from the lock and the model from the code and the data, on any machine, with two commands. A committed .venv would hold another machine's absolute paths, the .pth line among them, and a committed model would be a file whose origin the history cannot show. When. Before the first commit, and every time the project starts producing a new kind of file.

git status --short lists every file git does not track yet, ??, a folder as one line:

goku@machine:~/dev/listings-service$ git status --short
?? .gitignore
?? .python-version
?? README.md
?? data/
?? models/
?? pyproject.toml
?? src/
?? uv.lock

.venv and __pycache__ are missing already, from uv init's .gitignore, and models/ is listed. Add its section at the end of .gitignore, which then reads:

# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv

# What the code produces, rebuilt by running it
models/

git status --short now lists the same files without models/. Commit the project, from the repository uv init created on the main branch Module 0's setting names:

git add .
git commit -m "feat: the price script as a package"
[main (root-commit) aeb0076] feat: the price script as a package
 10 files changed, 3456 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 .python-version
 create mode 100644 README.md
 create mode 100644 data/listings.csv
 create mode 100644 pyproject.toml
 create mode 100644 src/listings/__init__.py
 create mode 100644 src/listings/data.py
 create mode 100644 src/listings/features.py
 create mode 100644 src/listings/train.py
 create mode 100644 uv.lock

Your commit's hash differs from aeb0076, because a hash covers the commit's time as well as its files. Ten files: the 3,001 lines of listings.csv and the 341 of uv.lock are most of the 3,456 insertions. listings.csv is 222,138 bytes that never change, so it belongs in the repository beside the code that reads it, and the data/ folder is where Module 6 keeps every table the project reads.

DON'T:

  • DON'T add uv.lock to .gitignore. It is the one file that tells a clone which eight unpinned packages to install, and every one of their versions.
  • DON'T commit models/. Its one file is 5,554 bytes that uv run python -m listings.train writes again in seconds, and a committed copy goes stale the moment the code or the data changes.

Lab Exercises

Lab Exercise 1: The Lock and the Cutoff

Clone the project into ~/dev/lock-check, run from ~/dev with git clone listings-service lock-check, which copies a local repository as it copied one from GitHub in Module 0. Work only in the clone. Delete its uv.lock, run uv lock, and check with git status --short whether the new lock differs from the committed one. Then delete the [tool.uv] table and its line from pyproject.toml and run uv lock again, and check again. Finally delete uv.lock once more, run uv lock, and read git diff --stat and uv tree. Say in one sentence after each step what decided the versions, and name every package whose version moved in the last step.

Lab Exercise 2: A Clone Trains the Same Model

From ~/dev, clone the project into ~/dev/listings-clone and list what the clone holds with ls -a. Train in the clone with uv run python -m listings.train, compare its models/price_pipeline.joblib with the original's using cmp, and run git status --short in the clone. Then, from ~/dev, train the clone once more without changing folder first. Say which of the clone's files came from git, which from the lock and which from the run, and why git status printed what it printed.


Solution 1: The Lock and the Cutoff
cd ~/dev
git clone listings-service lock-check
cd lock-check
rm uv.lock
uv lock
git status --short
Using CPython 3.12.8
Resolved 12 packages in 43ms

git status --short printed nothing: a lock made from nothing, with the cutoff, is the committed lock byte for byte. The three pins decided pandas, scikit-learn and NumPy, and the cutoff decided the other eight, the newest that each had uploaded before 2026-10-20. Delete these two lines from pyproject.toml, with the blank line above them:

[tool.uv]
exclude-newer = "2026-10-20T00:00:00Z"
goku@machine:~/dev/lock-check$ uv lock
Using CPython 3.12.8
Resolving despite existing lockfile due to removal of global exclude newer
Resolved 12 packages in 7ms
goku@machine:~/dev/lock-check$ git diff --stat
 pyproject.toml | 3 ---
 uv.lock        | 3 ---
 2 files changed, 6 deletions(-)

uv resolved again because the cutoff was gone, and kept every version: the three lines uv.lock lost are its [options] table, the cutoff itself. The existing lock decided the versions, because uv lock keeps each locked version that still satisfies pyproject.toml. Now the same project with no lock and no cutoff:

rm uv.lock
uv lock
git diff --stat
uv tree
Using CPython 3.12.8
Resolved 11 packages in 10ms
 pyproject.toml |  3 ---
 uv.lock        | 32 ++++++++++----------------------
 2 files changed, 10 insertions(+), 25 deletions(-)
Using CPython 3.12.8
Resolved 11 packages in 1ms
listings v0.1.0
├── numpy v2.5.2
├── pandas v3.0.5
│   ├── numpy v2.5.2
│   └── python-dateutil v4.0.0
│       └── tzdata v2026.6
└── scikit-learn v1.9.1
    ├── joblib v1.6.0
    │   └── cloudpickle v3.1.2
    ├── narwhals v2.28.0
    ├── numpy v2.5.2
    ├── scipy v1.18.1
    │   └── numpy v2.5.2
    └── threadpoolctl v3.7.0

The day decided, here 2026-11-03. The three pinned libraries held. narwhals moved from 2.27.1 to 2.28.0, uploaded on 2026-10-27, tzdata from 2026.5 to 2026.6, uploaded on 2026-10-28, and python-dateutil from 2.9.0.post0 to 4.0.0, uploaded on 2026-11-01, a new major version that no longer needs six, which left the lock, and needs tzdata on every platform, which is why tzdata now sits in the tree on WSL. Eleven packages resolved instead of twelve. A later day can move more of them, which is the whole case for the cutoff: without it, the same pyproject.toml locks to a different environment depending on when you run uv lock.

DON'T:

  • DON'T conclude from the second step that the cutoff does nothing. The kept lock held every version, and the moment that lock was gone, three packages moved.
  • DON'T copy anything from lock-check back into the project. Its pyproject.toml and uv.lock are the experiment, and rm -rf ~/dev/lock-check removes it.
Solution 2: A Clone Trains the Same Model
cd ~/dev
git clone listings-service listings-clone
cd listings-clone
ls -a
.
..
.git
.gitignore
.python-version
README.md
data
pyproject.toml
src
uv.lock
goku@machine:~/dev/listings-clone$ uv run python -m listings.train
Using CPython 3.12.8
Creating virtual environment at: .venv
   Building listings @ file:///home/goku/dev/listings-clone
      Built listings @ file:///home/goku/dev/listings-clone
Installed 11 packages in 25ms
56,562 0.766
['models/price_pipeline.joblib']
cmp models/price_pipeline.joblib ../listings-service/models/price_pipeline.joblib
git status --short
cd ~/dev
uv run --directory listings-clone python -m listings.train
56,562 0.766
['models/price_pipeline.joblib']

cmp printed nothing, so the two models are identical byte for byte, and git status --short printed nothing, so the clone's working tree is clean after a run that created .venv, models/ and __pycache__/. The last command trained from ~/dev because --directory moved the process into the clone first. The clone's eight entries besides . and .. came from git: .git and the seven tracked names. .venv and its eleven packages came from the lock, created and installed by the first uv run before it ran the module, where uv run printed Installed 11 packages without listing them. models/ and __pycache__/ came from the run, and .gitignore names all three, which is why git status stayed silent.

DON'T:

  • DON'T run uv sync in a fresh clone before the first uv run. uv run makes the same environment from the same lock on its own, as Creating virtual environment at: .venv showed.
  • DON'T take a clean git status after a run as proof that the run wrote nothing. It wrote a model, an environment and compiled files, and .gitignore kept all three out of the list.

Theory Assessment

Q1: One Installed Package and the Same Error

The first uv run python train_price.py in the new project created .venv, printed Installed 1 package, and then failed with exactly the error the system python3 gave, No module named 'joblib'. Say what each of uv init's three flags decided, what the one package was, and why the project's own environment held no joblib.

Hint: Read dependencies in the pyproject.toml that uv init wrote, and ask what uv run installs when that list is empty.

Q2: Twelve, Eleven, Three

pyproject.toml names three libraries, uv.lock holds twelve packages, and uv sync installed eleven on WSL. Account for each difference, and say what the lock carries that writing all eleven installed versions into pyproject.toml with == would not.

Hint: Count the project itself in each number, then find the one entry of the lock that carries a marker.

Q3: The Import Worked, the Read Did Not

From ~/dev/listings-service/src, uv run python -m listings.train imported every module and then raised FileNotFoundError for data/listings.csv. From ~/dev/listings-service, python3 -m listings.train failed before any module of the package ran. Explain each failure, and say what the second command does when the package is laid out flat, with listings/ directly in the project folder.

Hint: Imports go through sys.path, which -m starts with the current folder and .venv's .pth file extends, while pd.read_csv resolves a relative path against the working directory alone.

Q4: Two Teammates, One Commit

Two teammates check out the same commit. One runs uv pip install scikit-learn==1.9.0 and uv pip install rich==14.0.0 in the project, then uv run python -m listings.train. The other runs uv sync first, then the same training command. Say which scikit-learn each training run used, whether import rich still works for each afterwards, whether their two model files can match, and which of the two commands guarantees that .venv holds exactly the lock.

Hint: uv run checks every package the lock names before each command. Ask what it does with a package the lock does not name at all.


Answer 1:

--package gave the project a src/listings/ package and a build backend, so it is installed into its environment like any library, --name listings named the project and the package after the import name instead of the folder's listings-service, and --python 3.12.8 wrote 3.12.8 into .python-version, which uv run read when it created .venv, and >=3.12.8 into requires-python. The one package installed was listings itself, built from src/ and installed editable. joblib was absent because dependencies = []: a uv project's environment holds what pyproject.toml declares and what that pulls in, so with nothing declared, the right environment and the system Python fail alike, and the fix is uv add, not an install into .venv by hand.

DON'T answer a ModuleNotFoundError inside a uv project with uv pip install. The library belongs in dependencies, where uv add writes it, locks it and installs it in one step.

Answer 2:

pyproject.toml names the three libraries the project pins. The lock adds the eight packages those three need in turn, cloudpickle, joblib, narwhals, python-dateutil, SciPy, six, threadpoolctl and tzdata, and the project itself, which makes twelve. uv sync installed eleven: the project and ten libraries, because tzdata's entry carries marker = "sys_platform == 'emscripten' or sys_platform == 'win32'", so WSL, which is Linux, skips it, and a native Windows machine installs it from the same lock. Eleven == lines in pyproject.toml would fix the versions for this machine only, with no tzdata for Windows, and would leave out what the lock adds: the source of every file and its SHA-256 hash, checked before anything is installed, the graph uv tree draws, and the platform condition on each package. They would also make the project choose the versions of eight libraries it never chose, each one the lock already holds at the version the three need.

DON'T read the installed count as the lock's size. The lock resolves for every platform, and .venv holds this machine's share of it.

Answer 3:

From src/, uv run searched upward for pyproject.toml, found the project and used its .venv, whose .pth file puts src/ on sys.path, so listings, pandas and scikit-learn all imported. pd.read_csv("data/listings.csv") then resolved the path against the working directory, src/, where no data/ folder exists. From the project folder, python3 is the system's interpreter, with no .pth file and no libraries, and -m put the project folder first on sys.path, where there is no listings/, only src/, so the package was not found. Laid out flat, the second command gets further and fails worse: -m finds listings/ in the current folder, starts running the project's code on an interpreter without its libraries, and stops at train.py's import joblib, which is what a flat copy of this project did.

DON'T move the package out of src/ to make a bare python3 find it. It would find the code and not the libraries, and the src layout is what made that mistake fail before any of the project's code ran.

Answer 4:

The first teammate trained with scikit-learn 1.9.1: the lock names scikit-learn at 1.9.1, so uv run uninstalled 1.9.0 and installed 1.9.1 before running, as it printed Uninstalled 1 package and Installed 1 package in a clone of this project. rich still imports for them, because the lock does not name it, and uv run leaves a package it does not name where it is. The second teammate's uv sync removed rich and the three libraries it brought, so import rich fails for them, and their training ran on the locked set as well. Both runs used the lock's version of every library the code imports, so their model files can match byte for byte, as the clone's did, and the leftover rich is nothing the pipeline imports. Only uv sync guarantees that .venv holds exactly the lock: uv run guarantees that every locked package is at its locked version, which is enough to train correctly and not enough to say what else the environment holds.

DON'T treat a successful uv run as proof that .venv matches the lock. It matched every package the lock names and left rich and its three libraries in place.


Assignment

Assignment: One Command for the Team

The team wants to train with one command from wherever they stand in the project, the way they open a terminal in VS Code inside src/listings/ and run it there. Work in ~/dev/listings-clone, Lab Exercise 2's clone, so the project itself stays as this lesson left it.

Two things this lesson did not use. A [project.scripts] table in pyproject.toml maps a command name to a function, name = "package.module:function", and when uv installs the project it writes that command into .venv/bin/, where uv run name finds it and calls the function. And every module has __file__, the path of its own .py file, so in src/listings/data.py, Path(__file__).resolve().parents[2] is the folder two levels above src/listings/, the project root. uv run --no-editable installs the project as a plain copy into .venv instead of the .pth link, for that one command.

Add a train-price command that calls listings.train:main, run it from the clone's root and then from src/listings/, and read the failure. Then anchor DATA_PATH and MODEL_PATH to the project root through data.py's own __file__, with ROOT defined once in data.py and imported by train.py, and run the command from src/listings/ again. Finally run it once from the root with uv run --no-editable train-price. Show the whole change with git diff, and end with a comment saying why the anchored paths failed in the last run, and what that says about anchoring a path to a module's file instead of running from the project root.

Hints:

  • uv run notices that pyproject.toml changed and reinstalls the project before running, so the new command exists from its first uv run.
  • Path(__file__) follows wherever the module's file is installed, so print ROOT from both kinds of install before you trust it, uv run python -c "import listings.data as d; print(d.ROOT)" with and without --no-editable.

Solution:

Add the table to the clone's pyproject.toml, between dependencies and [build-system]:

[project.scripts]
train-price = "listings.train:main"
goku@machine:~/dev/listings-clone$ uv run train-price
   Building listings @ file:///home/goku/dev/listings-clone
      Built listings @ file:///home/goku/dev/listings-clone
Uninstalled 1 package in 0.30ms
Installed 1 package in 1ms
56,562 0.766
['models/price_pipeline.joblib']

The command runs from the root. From src/listings/ its traceback opens in .venv/bin/train-price, line 12, sys.exit(main()), the small launcher uv wrote, goes through train.py and data.py into pandas, and ends:

goku@machine:~/dev/listings-clone/src/listings$ uv run train-price
FileNotFoundError: [Errno 2] No such file or directory: 'data/listings.csv'

A command is a new way in, and it changed nothing about where the relative path is looked up. The anchored paths, as git diff shows the whole change:

diff --git a/pyproject.toml b/pyproject.toml
index 95c3741..252f8e2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,9 @@ dependencies = [
     "scikit-learn==1.9.1",
 ]
 
+[project.scripts]
+train-price = "listings.train:main"
+
 [build-system]
 requires = ["uv_build>=0.13.0,<0.14.0"]
 build-backend = "uv_build"
diff --git a/src/listings/data.py b/src/listings/data.py
index 42d1a1c..8e23520 100644
--- a/src/listings/data.py
+++ b/src/listings/data.py
@@ -1,9 +1,12 @@
 """Load the listings and split them the one way every lesson uses."""
 
+from pathlib import Path
+
 import pandas as pd
 from sklearn.model_selection import train_test_split
 
-DATA_PATH = "data/listings.csv"
+ROOT = Path(__file__).resolve().parents[2]
+DATA_PATH = ROOT / "data" / "listings.csv"
 NUMERIC = ["area_sqm", "year_built", "distance_km", "rooms", "bathrooms"]
 NOMINAL = ["neighborhood", "property_type", "energy_rating"]
 LEVELS = ["poor", "fair", "good", "excellent"]
diff --git a/src/listings/train.py b/src/listings/train.py
index 2b5b167..4b84995 100644
--- a/src/listings/train.py
+++ b/src/listings/train.py
@@ -1,14 +1,12 @@
 """Fit the price pipeline on the training rows, score it, save it."""
 
-from pathlib import Path
-
 import joblib
 from sklearn.metrics import mean_absolute_error, r2_score
 
-from listings.data import load, split
+from listings.data import ROOT, load, split
 from listings.features import build_pipeline
 
-MODEL_PATH = Path("models/price_pipeline.joblib")
+MODEL_PATH = ROOT / "models" / "price_pipeline.joblib"
 
 
 def evaluate(pipe, X, y):

train.py no longer builds a Path itself, so its pathlib import goes. From src/listings/, and then from the root:

goku@machine:~/dev/listings-clone/src/listings$ uv run train-price
56,562 0.766
['/home/goku/dev/listings-clone/models/price_pipeline.joblib']
goku@machine:~/dev/listings-clone/src/listings$ cd ../.. && uv run --no-editable train-price
FileNotFoundError: [Errno 2] No such file or directory: '/home/goku/dev/listings-clone/.venv/lib/python3.12/data/listings.csv'

The last line is the end of the --no-editable run's traceback, after uv's own lines rebuilt and reinstalled the project. The second hint's check prints /home/goku/dev/listings-clone/.venv/lib/python3.12 under --no-editable and /home/goku/dev/listings-clone without it, and the next plain uv run put the editable install back. The comment that ends the solution:

# The anchored paths name the folder two levels above wherever data.py
# is installed. The editable install leaves data.py in src/listings, so
# ROOT is the project. A plain copy puts data.py in .venv/lib/python3.12/
# site-packages/listings, so ROOT became .venv/lib/python3.12, where no
# data exists. Anchoring to __file__ ties the data's location to how the
# code was installed. Relative paths with every command started in the
# project root, or named with uv run --directory, hold for both installs.

The clone keeps the change uncommitted, and ~/dev/listings-service never saw it, which is the point of making it in a clone: the project stays on relative paths, and every command in this module starts from its root.

DON'T anchor a data path to __file__ and call it portable. It held for the editable install from the project folder and pointed into .venv/lib/python3.12 the moment the package was installed as a copy.


Additional Resources

Optional, and nothing later depends on either. Worth a look for the longer argument behind src/, or for every way uv can be told what a resolution may pick.

  • Python Packaging User Guide - src layout vs flat layout. Why a flat layout lets the copy in the current folder shadow the installed package, and the section "Running a command-line interface from source with src-layout" on what the src layout costs.
  • uv documentation - Resolution, "Reproducible resolutions". How exclude-newer compares each file's upload time against the cutoff, the date formats it accepts, and the per-package and per-index forms of the same cutoff.

Summary

The script never changed what it computed. The same rows went through the same steps and came out as the same 56,562 and the same 5,554 bytes, and everything this lesson built sits around that computation rather than inside it: a file that says which libraries it needs, a lock that says which packages it got and from which files, a cutoff that makes a resolution on any later day pick what this one picked, and a package whose pieces other programs can import. What used to be a fact about one machine's shared environment became a fact written into the folder.

Most of what went wrong was a question of where. The same import failed on the system Python and in an empty project for two different reasons, a command started one folder too deep imported everything and found nothing to read, and a copy of the package laid out flat ran the project's code on an interpreter without its libraries. Imports follow sys.path and files follow the working directory, and the src layout and the habit of starting every command from the project root are the two rules that keep those apart.

The clone is the test the whole lesson aims at. A folder that holds only what git kept trained the same model, byte for byte, with nothing activated and nothing installed by hand, while the lab that dropped the cutoff showed how little it takes for the same pyproject.toml to become a different environment. From here on, every new library, test and service enters the project through those same files, and the clone keeps proving they are enough.


Cheat Sheet

Part A - Reference Table

Task Command or file Note
Start a project uv init --package --name listings --python 3.12.8 src/listings/, uv_build, git repository
Interpreter .python-version 3.12.8, read by every uv command
Declare a library uv add pandas==3.0.5 writes dependencies, locks, installs
Without == uv add httpx only a lower bound, >=0.28.1
The cutoff [tool.uv] exclude-newer = "2026-10-20T00:00:00Z" files uploaded before it, any day
The lock uv.lock every package, version, file and hash, all platforms
Graph uv tree this machine's packages, each under its parent
Platform-only marker = "sys_platform == 'win32'" locked, installed only there
Rebuild rm -rf .venv && uv sync exactly the lock
Check the lock uv lock --check exit 1 when pyproject.toml changed
Run uv run python -m listings.train from the project root
uv run before each command repairs what the lock names, keeps extras
uv sync removes what the lock does not name rich and three libraries removed
Editable install .venv/lib/python3.12/site-packages/listings.pth one line, the src/ path
From a subfolder FileNotFoundError: ... 'data/listings.csv' imports work, paths follow the working directory
Bare python3 No module named 'listings' not .venv's interpreter
From elsewhere uv run --directory ~/dev/listings-service ... --project keeps the working directory
Ignored .venv, __pycache__/, models/ rebuilt by a command
Committed pyproject.toml, uv.lock, .python-version, README.md, src/, data/ what a clone needs

Part B - Compact Code Block

uv init --package --name listings --python 3.12.8
uv add pandas==3.0.5 scikit-learn==1.9.1 numpy==2.5.2
uv tree                          # who needs what
uv lock --check                  # lock still matches?
rm -rf .venv && uv sync          # exactly the lock
uv run python -m listings.train  # from the project root
git clone listings-service listings-clone
# every import starts at the package
from listings.data import load, split
from listings.features import build_pipeline
X_train, X_test, y_train, y_test = split(load())
# fit on the training rows only
pipe = build_pipeline().fit(X_train, y_train)
←Previous: Module 6
Next: Lesson 2→

Questions about this lesson: #module-6 on Discord

Found an error in this lesson? Report it

Your progress
Current streak
🔥 0
Lessons marked today
0 / 3
Longest streak
0

Sign in

The bootcamp

  • Curriculum
  • How To
  • Pricing
  • Changelog
  • Future

The community

  • Discord
  • Reviews
  • Contact

Follow

  • Instagram
  • X
  • TikTok
  • Reddit

Legal

  • Terms
  • Privacy
  • Refunds

Coming soon on November 16, 2026

Theme