Module 1: Packaging A Project With Uv
Why a script is not the same thing as a package
Description
kiosko_report.py works. You ran it in lesson 1 and it gave you exactly 106.15 in total revenue over the week's forty orders. This lesson doesn't question that it works — it questions under what conditions it works, and what happens the moment those conditions shift a little: a different folder, a different machine, a different person on the team, a different Python version. You're going to reproduce, with Kiosko's real pipeline, a concrete bug that a loose script invites almost by design: the same file, without changing a single line, giving a silently different result depending on where you run it from.
Connection to the module. This lesson doesn't install anything yet — it's the lesson that earns the right for you to install something. Every problem it names here has, later in this module, a concrete piece that solves it: pyproject.toml (lesson 4) declares which Python version and which dependencies the project needs; the src/ layout (also lesson 4) gives the code a clear boundary between "this is the package" and "this isn't"; and uv.lock (lesson 3) pins the exact version of each piece so the result doesn't depend on what whoever ran it happened to have installed.
An analogy: the recipe memorized versus the recipe printed
A cook who's spent twenty years making the same dish can prepare it from memory, without looking at any paper — they know exactly how much salt, how much time, in what order. It works perfectly, as long as it's them cooking, in their kitchen, with their usual ingredients. The problem shows up the day they get sick and need someone else to cover their shift: that person doesn't have the recipe in their head, only the finished result they tasted once. They can try to guess the proportions, and the dish might come out close — or it might come out with double the salt, and nobody notices until a customer complains.
A printed recipe, on the other hand, doesn't depend on it always being the same person or the same kitchen. It says "200 grams of flour," not "a handful, like the one I use." Anyone who follows it — today, in a month, in another city — arrives at the same dish, because the recipe externalized the knowledge that used to live only in the cook's memory.
kiosko_report.py is the cook working from memory: the pipeline lives inside the head of whoever wrote it — "you have to be standing in this exact folder," "you need Python 3.11 or newer," nobody wrote it down anywhere. A package with pyproject.toml is the printed recipe: it declares, in a file anyone can read without running it, exactly what it needs to work. This lesson demonstrates the day the cook working from memory gets sick.
Worked example: the same script, two different results
Take kiosko_report.py as it stood at the end of lesson 1, and Kiosko's seven files orders_2026-08-03.csv through orders_2026-08-09.csv, in a folder called kiosko_data/. Run the script standing inside that folder, exactly as you did in lesson 1:
cd kiosko_data
python3 kiosko_report.py
rows_extracted=40 rows_valid=40 rows_rejected=0 rows_loaded=40
Total week revenue: 106.15
Now, without touching a single line of kiosko_report.py, run the exact same command from one level up — the folder that contains kiosko_data/, not the folder itself:
cd ..
python3 kiosko_data/kiosko_report.py
What to expect. The output, verified, is this:
rows_extracted=0 rows_valid=0 rows_rejected=0 rows_loaded=0
Total week revenue: None
Read it twice, because it's exactly the worst kind of bug: no error, no traceback, no red flag anywhere. The script finishes, prints two lines shaped like a valid result, and states, with total confidence, that Kiosko sold nothing this week. If someone copied Total week revenue: None into a report without checking the row count, they'd hand over a completely fake zero, with no alarm to stop them along the way.
Diagram: why this happens, exactly
flowchart TD
A["extract_orders(folder='.', ...)\nthe '.' means 'the terminal's current folder'"] --> B{"Where is the terminal\nstanding right now?"}
B -->|"inside kiosko_data/"| C["Path('.').glob('orders_*.csv')\nfinds 7 files"]
B -->|"one level up"| D["Path('.').glob('orders_*.csv')\nfinds NO files at all"]
C --> E["40 rows extracted -> normal pipeline"]
D --> F["0 rows extracted -> sqlite SUM() over empty table -> None"]
F --> G["No error. No traceback.\nJust a silently wrong number."]
The code responsible is the exact same code you already know from foundations: extract_orders(".", day, day), inside kiosko_report.py's main loop. The argument "." means, literally, "the terminal's current folder at the moment of execution" — not "the folder where kiosko_report.py lives," which is what almost anyone would assume by instinct. Python never assumes you mean the second thing; the dot means exactly the first, always. And since Path(".").glob("orders_*.csv") on a folder without those files simply returns an empty list — it raises no error — the whole pipeline runs "fine" over zero rows, until SUM(revenue) in SQLite, over a table with no rows inserted, returns NULL — which Python's driver translates as None.
Going deeper: three real problems, not just the folder one
The folder bug is the easiest to reproduce, but it isn't the only thing a loose script drags along. It's worth naming the other two precisely, because each one has a specific piece of this module that solves it:
1. No declared environment. kiosko_report.py doesn't say, anywhere, which Python version it needs. It runs on Python 3.10, on 3.12, probably on 3.9 — until one day it doesn't, because it uses some new syntax (int | None, say, which needs Python 3.10+) and nobody knows why, because nothing in the project declared the requirement. Worse: if tomorrow this pipeline needed an external library — duckdb, say, once you reach module 5 — every person on the team would have to install it by hand, with pip install duckdb, and nothing would guarantee they all end up with the same version. This module's lesson 4 solves this with requires-python in pyproject.toml; lesson 3 solves it for dependencies with uv.lock.
2. No boundary between "the package" and "anything else." If tomorrow someone creates, in the same folder, a test file called transform.py to experiment with an idea — with no relation to the pipeline — that file coexists, with no separation at all, alongside Kiosko's real code. There's no structure that says "this is the installable package" and "this is just a loose file someone left here." Lesson 4 solves this with the src/ layout.
3. No clean way to reuse a single function somewhere else. If you wanted to use extract_orders() in a completely different script — a quick test, an exploratory notebook — your only option today is to copy and paste those nine lines, because there's no import that works reliably between two loose files that don't share any structure. Copy-pasting code is, over time, the surest way to end up with two versions of the same function that quietly drift apart without anyone noticing. Lessons 5 and 6 solve this by turning each piece into a real importable module: from kiosko_pipeline.extract import extract_orders, with nothing copied.
Common mistakes
Fixing the folder bug with os.chdir() inside the script. What happens: someone, running into this lesson's bug, adds a line like os.chdir(Path(__file__).parent) at the top of kiosko_report.py, forcing Python to switch the working folder to wherever the file lives. Why it happens: it's the fastest fix that occurs to someone in a hurry, and it does fix this specific symptom. How to spot it: if your script changes the working folder of the entire process with os.chdir(), any other code that runs afterward in the same process — or any relative path the user expected to be relative to their folder, not the script's — can break in a new and more confusing way. How to fix it: os.chdir() patches the symptom, not the cause — the real cause is that the script never declared, anywhere, where its data comes from or what it assumes about its environment. The real solution — passing explicit paths, assuming nothing about the current folder — is, on purpose, the entire topic of module 7's lesson 3 (configuration with no hardcoded values), later in this guide; packaging by itself, in this module, doesn't solve this specific problem — it solves the other two the "going deeper" section names.
Thinking "packaging" is a synonym for "fixing every problem a script has." What happens: someone expects that, the moment this module ends, the folder bug disappears entirely, because the code "is now a package." Why it happens: it's tempting to treat "packaging" as a universal fix for any code fragility. How to spot it: if in lesson 7 you run uv run python -m kiosko_pipeline from a folder other than the project root and expect it to work with no adjustment, you're going to find a variant of the same problem — because, honestly, this module doesn't solve it yet. How to fix it: be precise about what each piece solves. Packaging solves the declared environment and the code boundary (problems 1 and 2 from "going deeper"); it doesn't, by itself, solve the fragility of a poorly thought-out relative path — that's explicit work for module 7.
Confusing "the bug didn't show up in my testing" with "the bug doesn't exist." What happens: someone tests kiosko_report.py a couple of times, always from the same folder out of habit, never sees the problem, and concludes the script is reliable. Why it happens: a bug that depends on an external condition (where you run something from) is invisible as long as that condition doesn't change — and in one person's day-to-day, it almost never does. How to spot it: the right question isn't "did it happen to me?", it's "what needs to be true for this to work, and who else knows it?". How to fix it: this lesson's real test — running the same command from two different folders — is exactly the kind of check that exposes these bugs before a teammate, or you yourself six months from now, discovers them the worst possible way: with a wrong business number already delivered.
Exercises
Exercise 1 — Reproduce the bug yourself. With Kiosko's seven files in a kiosko_data/ folder and kiosko_report.py inside it, run the script from three different locations: (a) inside kiosko_data/, (b) one level up with python3 kiosko_data/kiosko_report.py, and (c) inside an empty sibling folder with python3 ../kiosko_data/kiosko_report.py. Before running each one, predict whether it will work or not.
See solution
Only (a) works — rows_extracted=40, revenue 106.15. Options (b) and (c) both give rows_extracted=0 and Total week revenue: None, regardless of the fact that the path you gave python3 on the command line is indeed correct: the argument to python3 tells Python which file to run, but it doesn't change the process's working folder, which stays wherever you typed the command from. extract_orders(".", ...) uses that working folder, not the script's location — the exact source of the bug.
Exercise 2 — Name the problem, not the symptom. In one sentence each, describe the three real problems this lesson's "going deeper" section names (undeclared environment, no code boundary, no clean reuse), without using the word "folder" in any of the three.
See solution
Undeclared environment: nothing in the project states which Python version or which external libraries it needs, so every person who runs it depends on happening to have a compatible environment. No code boundary: there's no structure separating "the pipeline's code" from any other loose file someone drops in the same folder. No clean reuse: there's no way to use one of the pipeline's functions in another file without copy-pasting its code, with the risk that the two copies quietly diverge over time.
Exercise 3 — Argue the boundary. This lesson's first common mistake mentions that os.chdir() "fixes the symptom, not the cause," and that the real solution of explicit paths belongs to module 7, not this module 1. In 2-3 sentences, explain why it makes sense that this module does not solve the folder problem, even though it demonstrated it in such detail.
See solution
This module solves the project's structure — where the code lives, what it declares about itself — not yet runtime configuration — where the data comes from on each run; that's a different question, with its own decisions (environment variables, command-line arguments, sensible defaults), which module 7 covers in depth under the exact heading "configuration with no hardcoded values." Demonstrating it here, without solving it yet, still has value: understanding the problem precisely — before you have the tool to solve it — is what lets you recognize, in module 7, why the solution taught there is the right one and not just one more patch.
Summary and next step
In this lesson you reproduced, with Kiosko's real pipeline, a silent bug: the same kiosko_report.py, without changing a single line, went from 106.15 in revenue to None depending on which folder you ran it from — with no error flagging it at all. You named, precisely, three real problems a loose script has: no declared environment, no boundary between the package and any other file, and no clean way to reuse code. And you traced which lesson in this module — or which module later in the guide — solves each one.
Before moving on you should be able to: reproduce the folder bug in your own words, explaining exactly why extract_orders(".", ...) fails silently; and name, without looking at the lesson, the three real problems a loose script has.
Lesson 3 installs the first piece of the solution: uv, the Python project manager that's going to declare, explicitly and verifiably, what Kiosko's pipeline needs to run the same way on any machine.
Resources
- Python — official documentation for
pathlib.Path.glob(), the method responsible for this lesson's bug. docs.python.org/3/library/pathlib.html#pathlib.Path.glob. - Python — official documentation for
os.getcwd()and the concept of the current working folder, the bug's root cause. docs.python.org/3/library/os.html#os.getcwd. - Python Packaging Authority — "Why should I use a src layout?", the official discussion lesson 4 picks back up to solve the code-boundary problem. packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout.