GLaDOS Vol 1 · Her Voice
ch 01 / 99
Chapter 01

Project setup

What you are actually building

That machine is ninety-nine chapters away, and every one of them ends with something running. This volume builds her voice: by chapter 10 your laptop will hold a real conversation with you, in her voice, using a brain that lives on your disk. Later volumes give her memory, judgment, a body, and finally a house to run.

Today, though, she needs a home for her code. That sounds like housekeeping, and compared to voice cloning it is. It is also the difference between a project that survives to chapter 99 and one that dies in chapter 20 with a dependency error nobody can reproduce. So we do it properly, once, and never think about it again.

The oldest bug in the book

Here is the failure this chapter exists to prevent. Somewhere around volume 8 you will copy her code onto a different machine. If your environment is "whatever pip install happened to grab this year," the model that loads fine on your laptop will crash on the new board, because the two machines resolved different package versions on different days. "Works on my machine" has ended more hobby robots than soldering irons have.

The traditional fix is pip plus virtualenv, and it leaks. pip install grabs whatever is newest today and records nothing. People bolt on pip freeze, requirements.txt and pyenv: three tools papering over one missing idea, the lock file. A lock file records the exact version of every package you depend on, including the dependencies of your dependencies, so that any machine, any day, resolves to identical code.

uv is that idea built in from the start: one binary that pins your Python version, resolves dependencies, writes an exact uv.lock, and runs your code. The mental model to carry through all ten volumes: her project is defined by the files you commitpyproject.toml, uv.lock, and a deliberate folder tree — not by whatever state your machine has drifted into.

⚒ Tool — uv

uv manages Python projects: it installs Python itself, creates the project, adds dependencies, and locks all of it. We use it as a tool throughout; everything you need is uv init, uv add, and uv run. Install it once:

curl -LsSf https://astral.sh/uv/install.sh | sh

Then restart your shell so uv is on your PATH. If you would like the full story of modern Python tooling, Python Zero volume 4 teaches it in depth; nothing in this book requires it.

A project with a pinned Python

▣ Build · stage 1 — init, and pin
uv init GladOS
cd GladOS
uv python pin 3.11
$ uv init GladOS && cd GladOS && uv python pin 3.11
Initialized project `glados` at `/home/you/GladOS`
Pinned `.python-version` to `3.11`

uv init creates pyproject.toml (the project's identity and dependency list) and a stub main.py you can delete. The pin writes .python-version, and from now on uv run uses Python 3.11 here no matter what the rest of your machine prefers. Open pyproject.toml and set the floor explicitly:

[project]
name = "glados"
version = "0.1.0"
requires-python = ">=3.11"

Why 3.11? It is the version every library in this book supports, including two with slow-moving native dependencies you will meet in chapters 5 and 7. Newer Pythons work for most of the stack; when a voice library lags behind a fresh release, the pin is what keeps that from being your problem.

A layout declared as data

Her project needs six directories, and it will earn more as the book goes on: labs for the scripts you build per chapter, configs for the settings files that start mattering in volume 2, glados/data and glados/models for datasets and downloaded model weights, experiments for tuning runs, prompts for her personality files. You could create them by hand. Instead we write a script, for a reason worth two minutes: a layout created by hand exists only on your machine, but a layout created by a committed script is part of the project. Machine two runs the script and matches machine one exactly.

▣ Build · stage 2 — the smallest thing that runs

Create labs in your editor first, then save the file below inside it. A script cannot create the directory that must already exist to save that script. Run each command from the GladOS project root.

# labs/setup_dirs.py
from pathlib import Path

Path("labs").mkdir(exist_ok=True)
print("Created: labs/")
$ uv run python labs/setup_dirs.py
Created: labs/

One directory, then run it. The habit this book asks of you starts here: never write thirty lines and hope. Write three, run them, and build on ground you have tested.

The flag already matters. Without exist_ok=True, the second run of this script raises FileExistsError, because labs/ is already there. With it, creation is idempotent: safe to run any number of times, which is exactly what you want from a setup script you will run on every machine she ever lives on.

▣ Build · stage 3 — the whole layout, as a list
# labs/setup_dirs.py
from pathlib import Path

DIRS = ["labs", "configs", "glados/data", "glados/models", "experiments", "prompts"]

for d in DIRS:
    Path(d).mkdir(parents=True, exist_ok=True)
    print(f"Created: {d}/")
$ uv run python labs/setup_dirs.py
Created: labs/
Created: configs/
Created: glados/data/
Created: glados/models/
Created: experiments/
Created: prompts/

The structure is now a list you can read at a glance, and adding a directory later means adding one string. Note the second flag: glados/data is a nested path, and plain mkdir creates only the final component, assuming the parent already exists. parents=True builds the whole chain, glados first, then glados/data. Leave it off and you get the crash in the failure box below, exactly as written.

▣ Build · stage 4 — a typed function behind the guard
# labs/setup_dirs.py
from pathlib import Path

DIRS = ["labs", "configs", "glados/data", "glados/models", "experiments", "prompts"]

def build_structure(dirs: list[str]) -> int:
    created = 0
    for d in dirs:
        Path(d).mkdir(parents=True, exist_ok=True)
        print(f"Created: {d}/")
        created += 1
    return created

def main() -> None:
    count = build_structure(DIRS)
    print(f"\nProject structure ready — {count} directories.")

if __name__ == "__main__":
    main()
$ uv run python labs/setup_dirs.py
Created: labs/
Created: configs/
Created: glados/data/
Created: glados/models/
Created: experiments/
Created: prompts/

Project structure ready — 6 directories.

Two conventions arrive here and stay for the whole book. First, build_structure returns its count instead of only printing, so callers and tests can check the result; chapter 2 makes the case for why that distinction runs deeper than style. Second, the if __name__ == "__main__": guard means another file can import setup_dirs to reuse the function without the script creating folders as a side effect of the import. The typed signature (list[str], -> int) lets your editor flag a wrong argument before you ever run it.

Why this works: how mkdir walks a path

A directory can only be created inside a parent that already exists; that is a filesystem rule, not a Python one. Path.mkdir() exposes two flags that decide what happens at the edges of that rule, and the three combinations behave differently enough to be worth setting side by side:

  • mkdir() is strict: it creates exactly one level, raises FileNotFoundError if a parent is missing and FileExistsError if the directory is already there.
  • mkdir(parents=True) builds every missing parent top-down, but still refuses a directory that already exists.
  • mkdir(parents=True, exist_ok=True) builds the chain and accepts what is already there. Idempotent, and the combination this project uses everywhere.

Declaring the layout as a list and feeding it through one function splits the concerns cleanly: the structure is data you can read, and the behavior (create, idempotently, with parents) lives in exactly one place.

⚠ Worked failure — why does it die on glados/data?

This version looks fine and even works for the first two entries. The author dropped parents=True, and the flat folders created cleanly in testing:

for d in DIRS:
    Path(d).mkdir(exist_ok=True)   # forgot parents=True
    print(f"Created: {d}/")
$ uv run python labs/setup_dirs.py
Created: labs/
Created: configs/
Traceback (most recent call last):
  File "labs/setup_dirs.py", line 6, in <module>
    Path(d).mkdir(exist_ok=True)   # forgot parents=True
  File "/usr/lib/python3.11/pathlib.py", line 1116, in mkdir
    os.mkdir(self, mode)
FileNotFoundError: [Errno 2] No such file or directory: 'glados/data'

Read the trace from the bottom: the operating system refused to create glados/data because glados does not exist yet. The first two entries are flat, so plain mkdir survives them; the third is nested, and without parents=True Python will not build the chain. Partial success is the tell: a script that dies on entry three of six almost always has an assumption that the first two happened to satisfy.

◆ Note — what actually got committed

git status now lists pyproject.toml, .python-version, uv.lock and labs/setup_dirs.py. The first uv run creates or updates the lock file and syncs the environment; commit that lock file. Git tracks the setup script, not its empty directories.

Checkpoint, and what comes next

✓ Checkpoint — what you can now do
  • I can explain what uv.lock records that pip install does not, and why that matters the day her code moves to a second machine.
  • I can predict what the setup script prints on its second run, and why it does not error.
  • I know which of the two mkdir flags builds missing parents and which one forgives existing directories, and what each failure looks like without it.
  • I can say what the __main__ guard protects: importing setup_dirs defines functions, and creates nothing.
⚡ Exercises — try first, then reveal
Exercise 1 — break it on purpose. Delete the glados directory, remove parents=True from the script, and run it. Which entries succeed before it dies, and why those?

labs and configs succeed: both are flat paths whose parent (the project root) exists. The script dies on glados/data, the first nested path, with FileNotFoundError. Put the flag back and the run completes; run it once more to confirm the second pass is a clean no-op.

Exercise 2 — grow the layout. Volume 2 will need a glados/memory directory. Add it the way the design intends, and say what the script prints afterward.

One string in DIRS: "glados/memory". Nothing else changes; parents=True handles the nesting, and the run now prints seven Created: lines and "Project structure ready — 7 directories." That one-line change is what "structure as data" buys.

Exercise 3 — prove the guard. From the project root, run uv run python -c "import labs.setup_dirs" in a directory where you have deleted prompts/. Does prompts/ come back?

No. The import defines build_structure and main and runs neither, so nothing is created. The directory returns only when something calls the function: uv run python labs/setup_dirs.py, which enters through the guard. Side effects on import are how libraries surprise you; the guard is how yours never will.

The workspace is real: a pinned Python, a lock file waiting for its first dependency, a layout that rebuilds itself anywhere. Next chapter covers the handful of Python habits this build leans on hardest, starting with why a function that returns beats a function that prints, which sounds pedantic right up until chapter 10 composes five of them into her voice.