Modern Environment Management
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
Your code can stay unchanged and still break tomorrow. That sounds unfair, but the usual cause is not magic: it is dependency drift, which means the packages around your code changed while your code stayed still.
Modern environment management is how you stop that drift. You give each project its own package drawer, write down the tools inside it, and rebuild from that record.
The job of an environment
A Python environment is the Python interpreter plus the installed packages that interpreter can import. If your terminal runs one Python, but your editor runs another Python, you can get the classic "it works here, not there" problem.
You already met this in pip and virtual environments: a virtual environment is an isolated Python workspace with its own package folder. The course analogy still holds. A virtual environment is a labeled project drawer. Your score reporter and snack tracker do not share tools by accident.
venv is the standard-library module that creates virtual environments. virtualenv is a third-party tool, meaning a package installed from outside the standard library, that creates virtual environments too. virtualenv came first and is still common in older projects; venv is the built-in default you reach for unless a project tells you otherwise.
The old-school shape is still useful:
$ cd snack-tracker
$ python -m venv .venv
$ source .venv/bin/activate
(.venv) $ python -m pip install rich
(.venv) $ deactivateOn Windows PowerShell, activation uses .\.venv\Scripts\Activate.ps1 instead of source .venv/bin/activate. The idea is the same: activation is changing the current terminal so python and pip point at the project drawer.
That solves isolation. It does not solve the next problem: how does a teammate rebuild the same drawer next week?
Requirements are a start, not the finish
A dependency is a package your project needs in order to run. A direct dependency is one you choose by name, like rich for pretty terminal output. A transitive dependency is a package pulled in because one of your dependencies needs it.
A hand-written requirements.txt might list only the tools you asked for:
rich
pytestThat file says what you want. It does not say which exact versions were chosen. If rich releases a new version tomorrow, a fresh install may choose different code than your current drawer has.
Pinning exact versions improves the file:
rich==13.9.4
pytest==8.3.3The == pin says "use this exact version." That is better, but teams usually want a tool to create and update those pins. Real projects have many direct and transitive dependencies; writing the full inventory by hand is easy to get wrong.
Dependency managers do the bookkeeping
A dependency manager is a tool that records what your project asks for, chooses compatible package versions, writes a locked record, and installs from that record. Think of it as the clerk for your project drawer: you ask for tools, and it writes the inventory before stocking the drawer.
Three modern workflows show up often:
- Poetry is an all-in-one project tool that manages dependencies, a virtual environment, commands, and a lockfile.
- pip-tools is a small toolset that keeps the
pipandrequirements.txtworkflow, but adds a compile step that produces pinned requirements. uvis a fast modern tool that can create environments, add dependencies, lock versions, sync environments, and run commands.
You do not need all three in one project. Pick the one your team uses. For a new solo project, uv is a practical default because it covers the full daily loop. For a project that already installs from requirements.txt, pip-tools may fit with less change. For a project that already uses Poetry, use Poetry.
Here are the shapes you will see:
$ poetry add rich
$ poetry lock
$ poetry install
$ poetry run python app.py
$ pip-compile requirements.in
$ pip-sync requirements.txt
$ uv init snack-tracker
$ cd snack-tracker
$ uv add rich
$ uv lock
$ uv sync
$ uv run python app.pypyproject.toml is the standard Python project settings file; lesson 18.3 gives it the full treatment. Poetry and uv commonly record project dependencies there. pip-tools commonly starts from requirements.in, a small input file listing the dependencies you asked for, then compiles a pinned requirements.txt.
Syncing is making the current environment match the lock or pinned requirements. It may install missing packages and remove packages that no longer belong. That is stronger than "install whatever is missing," because the goal is an environment that matches the written record.
Lockfiles turn installs into rebuilds
A resolver is the part of a dependency tool that chooses package versions that satisfy all the project's rules. If you ask for rich and pytest, the resolver also checks what those packages need, then chooses a set that works together.
A lockfile is a generated file that records the exact dependency versions the resolver chose. It is the drawer inventory: not just "we need snacks," but "three apples, one tea, and the small honey jar."
Different tools use different lockfile names:
poetry.lock # Poetry's locked inventory
requirements.txt # pip-tools can generate a fully pinned inventory here
uv.lock # uv's locked inventoryYou normally do not hand-edit a lockfile. Change the input instead, then ask the tool to resolve again. For Poetry and uv, that usually means changing pyproject.toml through the tool. For pip-tools, that usually means editing requirements.in and compiling again.
A reproducible build is a setup another machine can rebuild with the same dependency versions. The promise is not "nothing can ever differ." Operating system, Python version, and hidden network issues still matter. The value is narrower: the package choices are written down, so a new drawer is stocked from the same inventory.
That matters when a test fails. If everyone installs from the same lock, you can focus on the code instead of asking whether the package set changed.
A safe modern habit
For application projects, programs you run as one product, commit the lockfile, meaning save it in project history. For library projects, packages other projects import, the trade-off is subtler; packaging and publishing in lesson 18.3 will separate those cases.
Until a project gives you a different standard, use this habit:
$ cd snack-tracker
$ uv init
$ uv add rich pytest
$ uv lock
$ uv sync --locked
$ uv run python -m pytest--locked means the command should use the existing lock instead of quietly changing it. That is useful before tests because it catches "the environment changed" as its own problem.
If you are not using uv, keep the same mental model:
- Create or activate the project environment.
- Record the dependencies you asked for.
- Generate or update the locked inventory.
- Rebuild environments from that inventory.
- Run project commands through that environment.
The tool names change. The professional habit does not: isolate first, lock second, rebuild from the lock.
Checkpoint
Answer all three to mark this lesson complete
You now have the dependency state under control before the project grows more files. Next, you shape those files into a real project layout, where code, tests, settings, and secrets each get a clear home.