# The AI Engineer's Toolkit: Python, uv, and a Sane Dev Setup

> Set up a Python AI engineering environment with uv, proper API key hygiene, and the minimal stack to start calling frontier models without the usual friction.

Canonical: https://ironclad.academy/ai-engineering/articles/ai-engineers-toolkit-python-setup | Difficulty: beginner | Topics: Fundamentals, LLMOps, Production, Career

## Key takeaways
- uv replaces pip, venv, and pip-tools in a single binary and is 10-100x faster at dependency resolution and installation.
- API keys in .env files with python-dotenv; .env in .gitignore; rotate immediately if ever committed — no exceptions.
- The minimal AI engineering stack is five packages: openai, anthropic, python-dotenv, pydantic, and jupyterlab — add orchestration frameworks only when you hit actual friction.
- Python 3.11 or 3.12 on every project; pin it in pyproject.toml so the version does not silently drift across machines.
- Notebooks for exploration; scripts for production — never use the same file for both purposes.
- The environment setup step is where most beginners lose an afternoon; getting it right once means every future project starts in minutes.

> **In one line:** A reproducible Python environment with uv, proper secret management, and the right five packages is all you need to start building AI systems — everything else is added friction.

**The friction problem.** Most "getting started with AI engineering" tutorials skip straight to the API call, leaving you with a globalenv pip install, a hardcoded API key, and no path to reproducibility. The next person who clones your repo spends two hours debugging version conflicts. This article is the setup step those tutorials omit — done right.

**The toolkit.** uv handles Python version management, virtual environments, and package locking in a single fast binary. A `.env` file plus `python-dotenv` keeps your keys out of source control. Five packages get you to a working baseline: `openai`, `anthropic`, `python-dotenv`, `pydantic`, and `jupyterlab`. From there you can call frontier models, validate outputs, and explore interactively without fighting toolchain issues.

**Key points.**
- uv is 10-100x faster than pip for package installation and handles lockfiles out of the box — it should be the default tool for new Python AI projects in 2026.
- API keys belong in `.env` files, not in code, not in shell history, not in Slack messages. A single accidental commit to a public repo can drain your OpenAI credits overnight.
- Python 3.11 or 3.12, pinned in `pyproject.toml`, prevents the silent version drift that causes "works on my machine" bugs.
- Notebooks for exploration, scripts for production — using a single sprawling notebook as your production pipeline is a technical debt trap.
- Resist adding LangChain or LlamaIndex until you have something to orchestrate; the abstractions cost you clarity before they save you code.

**Back-of-envelope: what bad setup costs.**

```
Average time lost per developer on bad Python env setup: ~3 hours/incident
Team of 5, one incident per month: 5 × 3 = 15 hours/month
At $80/hr fully-loaded engineer cost: $1,200/month wasted on package conflicts
Over a year: $14,400 in engineering time — for a problem uv solves in 30 seconds
```

```mermaid
flowchart TD
    A[New AI engineering project] --> B{Tool choice}
    B -->|uv| C["uv init my-project\nuv add openai anthropic\nuv run python main.py"]
    B -->|pip + venv| D["python -m venv .venv\nsource .venv/bin/activate\npip install -r requirements.txt\n— repeat for each dev"]
    B -->|conda| E["conda create -n env python=3.12\nconda activate env\npip install ...\n— CUDA packages may diverge"]
    C --> F["Reproducible lockfile\nSingle binary\nFast"]
    D --> G["Manual activation\nNo lockfile by default\nSlow installs"]
    E --> H["Good for CUDA binaries\nOverkill for API-only work\nSlow solves"]
    style C fill:#0e7490,color:#fff
    style F fill:#0e7490,color:#fff
```

**Key design decisions.**

| Decision | Recommended choice | Why |
| --- | --- | --- |
| Package manager | uv | 10-100x faster than pip; lockfile included; single binary |
| Python version | 3.11 or 3.12, pinned in pyproject.toml | SDK compatibility; 3.12 is ~25% faster than 3.10 |
| Secret management (dev) | .env + python-dotenv | Simple, standard, keeps keys out of source control |
| Secret management (prod) | Environment variables injected by CI/CD | No .env files in production containers |
| Notebooks vs scripts | Notebooks for exploration, scripts for pipelines | Separation of concern; notebooks don't belong in CI |
| Pydantic version | pydantic v2 (2.x) | Required by FastAPI, LangChain, anthropic SDK; v1 is EOL |

**If you have 60 seconds, say this.** "I use uv for every new Python project — it handles the virtual environment, pins Python itself with `requires-python`, and installs packages with a lockfile, all 10-100x faster than pip. API keys go in a .env file loaded by python-dotenv, that file goes in .gitignore immediately, and in production I inject them as environment variables from the CI secret store. The starting package list is five things: openai, anthropic, python-dotenv, pydantic, and jupyterlab. I keep notebooks for exploration and scripts for anything that runs in a pipeline. That setup takes about 10 minutes and it's reproducible across the whole team."



The day I accidentally pushed an OpenAI API key to a public GitHub repo was educational. Eleven minutes later, an automated scanner had found it, spun up a batch job in a region I had never used, and started billing at $0.015 per 1K output tokens. By the time GitHub's secret scanning sent me an email and I rotated the key, the bill was non-trivial. Nothing in my code was wrong. The bug was in how I set up the environment.

This article is the setup step every other tutorial skips. It takes about 20 minutes to do correctly and will save you a recurring 2-3 hour tax every time you start a new project or onboard a teammate.

## Why the environment step matters more than you think

AI engineering work involves credentials (API keys worth real money), fast-moving packages (the `anthropic` SDK released eight minor versions in 2025), and a tendency for "quick experiments" to turn into production code. Bad environment hygiene compounds: the notebook that started as a prototype becomes the thing generating embeddings for 50,000 users, still running on the version of `openai` you pip-installed globally six months ago, with the API key hardcoded in line 3.

Getting this right once means every project starts clean in ten minutes, your teammates can reproduce your environment exactly, and your keys are never at risk. Getting it wrong means you are debugging `ImportError: cannot import name 'AsyncOpenAI' from 'openai'` at 11pm before a demo.

## Choosing your package manager: uv wins in 2026

The Python packaging ecosystem has too many options. Here is the honest state of each.

**`pip` + `venv`** is what you already know. It works. The problems: no lockfile by default (so `pip install` produces different results on different machines unless you pin manually with `pip freeze > requirements.txt`), no Python version management (you install the Python separately via homebrew or pyenv), and it is slow — installing a project with GPU dependencies and ML libraries can take 5-10 minutes.

**`poetry`** added lockfiles and pyproject.toml management in 2018 and was the right answer for a long time. In 2026 it has one major weakness: speed. Poetry's dependency resolver is still significantly slower than uv's, and the `poetry install` command on a cold cache can take 60-90 seconds for a moderately sized AI project.

**`conda`** is excellent for one specific use case: managing non-Python native binaries like CUDA toolkit, cuDNN, and NCCL alongside Python packages. If you are doing local GPU inference with PyTorch and need CUDA 12.4 pinned exactly, conda handles that dependency graph correctly. For pure API-calling AI engineering work — which is most of what this course covers — conda is overengineered and slower than the alternatives.

**`uv`** is a Rust-based package manager from Astral (the company behind `ruff`) that replaces pip, venv, pip-tools, and pyenv in a single binary. It installs packages 10-100x faster than pip by parallelizing downloads and using a global cache, manages Python versions natively, and produces a lockfile (`uv.lock`) by default. As of mid-2026 it is the fastest Python package manager available and has become the default recommendation from most of the AI engineering community.

Install it once, system-wide:

```bash
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
```

## Setting up a new AI engineering project

Ten minutes from scratch to a running API call:

```bash
# Create and initialise a new project (--package gives you the src/ layout
# and a build-system, so uv sync installs your code into the environment)
uv init --package my-ai-project
cd my-ai-project

# Pin Python version (pick 3.11 or 3.12)
uv python pin 3.12

# Add your baseline packages
uv add openai anthropic python-dotenv pydantic jupyterlab

# uv creates a virtual environment automatically and writes uv.lock
# Run anything inside the managed environment with uv run
uv run python -c "import openai; print(openai.__version__)"
```

The resulting `pyproject.toml` looks like this:

```toml
[project]
name = "my-ai-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "openai>=1.35.0",
    "anthropic>=0.30.0",
    "python-dotenv>=1.0.0",
    "pydantic>=2.7.0",
    "jupyterlab>=4.2.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

The `[build-system]` table is what `--package` buys you: without it, `uv sync` treats the project as a loose collection of scripts and never installs `my_ai_project` into the environment — and `import my_ai_project` fails everywhere except the project root.

Commit `pyproject.toml` and `uv.lock`. Anyone who clones the repo runs `uv sync` and has the exact same environment in under 30 seconds.

## API key hygiene: the non-negotiable part

The model providers charge by the token. If your key leaks, someone else runs your bill up. This is not hypothetical — automated scanners watch public GitHub repos in near-real-time.

**Rule 1: keys in `.env`, never in code.**

Create `.env` at the project root:

```bash
# .env
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
```

Add `.env` to `.gitignore` immediately — before you add anything to it:

```bash
echo ".env" >> .gitignore
git add .gitignore
git commit -m "add .gitignore"
```

Load the `.env` file at the top of every script:

```python
from dotenv import load_dotenv
import os

load_dotenv()  # reads .env if it exists, does nothing if it doesn't

openai_key = os.environ["OPENAI_API_KEY"]   # raises KeyError if missing — intentional
anthropic_key = os.environ["ANTHROPIC_API_KEY"]
```

Using `os.environ["KEY"]` (bracket syntax) rather than `os.environ.get("KEY")` is deliberate: it fails loudly if the key is missing, rather than passing `None` silently to an API constructor and producing a confusing auth error 10 lines later.

**Rule 2: different keys for development and production.**

Both OpenAI and Anthropic let you create multiple API keys. Use one key per environment (dev, staging, prod) and set usage limits on the dev key (OpenAI's dashboard lets you cap at $10/month for a personal dev key). If the dev key leaks, the blast radius is a $10 charge, not an uncapped production bill.

**Rule 3: in production, no .env files.**

In a production container or CI environment, inject keys as environment variables from your secret store — GitHub Actions secrets, AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault. The code that reads `os.environ["OPENAI_API_KEY"]` stays identical; only the source of the variable changes.

```mermaid
flowchart LR
    DEV["Development\n.env file\n(git-ignored)"] -->|same code| APP["Application\nos.environ['OPENAI_API_KEY']"]
    CI["CI / CD\nGitHub Actions secret"] -->|injected at runtime| APP
    PROD["Production\nAWS Secrets Manager\nor GCP Secret Manager"] -->|injected at container start| APP
    style APP fill:#0e7490,color:#fff
    style DEV fill:#1e3a5f,color:#fff
    style CI fill:#1e3a5f,color:#fff
    style PROD fill:#1e3a5f,color:#fff
```

**Rule 4: if a key is ever committed, rotate it immediately.** Not after you finish your current work. Now. On public repos, GitHub's push protection will warn you before the push goes through; bypass it and scrapers find the key within minutes. Private repos only get push protection with paid Advanced Security — and while external scanners can't see them, treat a committed key as compromised anyway: contractors, CI logs, and a future flip to public all expose the git history. Rotate.

## Your first API call

With the environment set up, the first call to each provider is straightforward:

```python
# main.py
from dotenv import load_dotenv
from openai import OpenAI
from anthropic import Anthropic

load_dotenv()

# OpenAI
oai = OpenAI()  # reads OPENAI_API_KEY from environment automatically
response = oai.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "What is 2 + 2? Answer with just the number."}],
    max_tokens=10,
)
print("OpenAI:", response.choices[0].message.content)

# Anthropic
ant = Anthropic()  # reads ANTHROPIC_API_KEY from environment automatically
message = ant.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=10,
    messages=[{"role": "user", "content": "What is 2 + 2? Answer with just the number."}],
)
print("Anthropic:", message.content[0].text)
```

Run it:

```bash
uv run python main.py
# OpenAI: 4
# Anthropic: 4
```

Total cost of that script: about $0.000015. The `max_tokens=10` cap prevents any accidental runaway generation during setup testing.

## The minimal stack: five packages and why each one

```
openai          — official Python SDK for GPT-4o, GPT-4o-mini, o3, and OpenAI embeddings
anthropic       — official Python SDK for Claude Sonnet/Opus/Haiku 4.x
python-dotenv   — loads .env into os.environ; one line at the top of every script
pydantic        — data validation and structured output parsing; required by most AI frameworks
jupyterlab      — notebooks for exploration and visualization
```

That's it for the first week. Some packages you will see in tutorials that you should not install yet:

**`langchain` / `langchain-openai`:** LangChain is a powerful orchestration framework, but it abstracts away the underlying API calls in ways that make debugging harder when you are still learning what those calls do. Once you have called the APIs directly twenty times and felt the friction of building your own retry logic, the abstractions will make sense. Before that, they obscure more than they help. See the [context engineering article](/ai-engineering/articles/prompt-to-context-engineering) for when orchestration becomes genuinely useful.

**`tiktoken`:** Useful for counting tokens before sending a request. Add it when you start building applications where context length matters — not on day one.

**`torch` / `transformers`:** These are for running models locally. If you are calling the OpenAI or Anthropic APIs, you do not need PyTorch installed. Adding it pulls in gigabytes of dependencies and can complicate your environment significantly. Wait until you actually need local inference.

## Notebooks vs scripts: the sharp line

Jupyter notebooks are the right tool for:
- Exploring a new API's response structure
- Visualising token distributions or embedding spaces
- Running a one-off evaluation against a small dataset
- Writing an interactive tutorial (like the ones on this site)

Python scripts are the right tool for:
- Anything that runs in CI
- Anything that runs on a schedule
- Anything another service calls
- Anything that produces outputs other systems depend on

The mistake I see constantly: a notebook that starts as "let me see what this embedding model returns" grows into "let me process all 10,000 documents" grows into "this runs nightly and feeds our search index." Notebooks have no good story for testing, logging, error handling, or deployment. The moment a notebook becomes something that needs to run reliably, rewrite it as a script.

```mermaid
flowchart TD
    EXP["Exploration\n(notebook)"] -->|"Concept proven\nwants to run repeatedly"| SCRIPT["Refactor to script\n(proper module)"]
    SCRIPT -->|"Needs to run in CI\nor production"| PIPELINE["Part of a pipeline\n(tested, logged, monitored)"]
    EXP -->|"One-off analysis\nor visualization"| DONE["Done — stays a notebook\narchived with outputs"]
    style EXP fill:#1e3a5f,color:#fff
    style SCRIPT fill:#0e7490,color:#fff
    style PIPELINE fill:#15803d,color:#fff
    style DONE fill:#374151,color:#fff
```

## Project structure that won't embarrass you in a month

Start simple:

```
my-ai-project/
├── pyproject.toml          # dependencies, managed by uv
├── uv.lock                 # exact versions, committed to git
├── .env                    # API keys — git-ignored
├── .gitignore              # includes .env
├── README.md
├── src/
│   └── my_ai_project/
│       ├── __init__.py
│       └── main.py         # your application code
├── notebooks/
│   └── exploration.ipynb   # experiments, never runs in CI
└── tests/
    └── test_main.py
```

The `src/` layout (putting your code under a subdirectory rather than at the project root) is a minor discipline that pays off immediately when you add tests. With a flat layout, `import my_ai_project` resolves to whatever folder sits in your working directory — even when the package was never installed — which silently masks packaging mistakes. The `src/` layout forces imports to go through the installed package, so your tests exercise what anyone who runs `uv sync` actually gets.

The Makefile below expects `pytest` and `ruff`, which are development tools, not runtime dependencies — add them to uv's dev group so they install for developers but stay out of production:

```bash
uv add --dev pytest ruff
```

A `Makefile` at the root keeps the commands muscle-memory consistent:

```makefile
.PHONY: install test lint run

install:
	uv sync

test:
	uv run pytest tests/

lint:
	uv run ruff check src/ tests/

run:
	uv run python -m my_ai_project.main
```

New teammate? They run `make install` and then `make run`. Done.

## Worked cost estimate: what the minimal stack actually costs to run

```
Setup assumptions:
- OpenAI gpt-4o-mini: $0.15/M input tokens, $0.60/M output tokens (illustrative, mid-2026)
- Anthropic claude-haiku-4: $0.25/M input tokens, $1.25/M output tokens (illustrative, mid-2026)
- Typical beginner exploration: 100 API calls/day, avg 500 input + 200 output tokens each

Daily token usage:
  Input:  100 calls × 500 tokens = 50,000 input tokens = 0.05M tokens
  Output: 100 calls × 200 tokens = 20,000 output tokens = 0.02M tokens

Daily cost (gpt-4o-mini):
  Input:  0.05M × $0.15 = $0.0075
  Output: 0.02M × $0.60 = $0.0120
  Total:  ~$0.02/day

Monthly cost at this rate: ~$0.60/month

At 10× scale (1,000 calls/day, 1,000 avg tokens):
  Monthly cost: ~$18-30/month depending on model

→ Set a $10 soft limit on your dev key to catch accidental loops.
  A while True: client.chat.completions.create(...) with no break,
  at ~1 small call/second, burns roughly $0.70/hour at gpt-4o-mini
  pricing — left running overnight, it eats most of a $10 cap.
  Swap in a frontier model with a large max_tokens and the same
  loop hits the cap in under an hour.
```

These numbers illustrate why API key hygiene matters even for personal projects. A key leak to an attacker who uses it for a large batch job can run up hundreds of dollars in minutes — not because your key has high limits, but because they will run until they hit the limit you set (or, worse, if you forget to set one).

## What breaks: the failure modes this setup prevents

**Version drift.** Without a lockfile, `uv sync` / `pip install` on a new machine may install different minor versions. The `anthropic` SDK changed the response structure between 0.20 and 0.30. If one developer is on 0.20 and another is on 0.30, `message.content[0].text` works on one machine and raises `AttributeError` on the other. `uv.lock` prevents this.

**Global package pollution.** Installing into the global Python environment (the one that runs your OS tooling on some Linux distros) causes conflicts that are extremely hard to debug. `pip install openai` globally on Ubuntu can break system tools that depend on a different requests version. uv's virtual environment is isolated by default.

**Key commit.** If you add your API key to `main.py` and commit it before adding `.gitignore`, GitHub will flag it — but the key is already in the git history. `git log` exposes it to anyone with repo access. Rotation is the only fix. Adding `.gitignore` and `.env` before writing any code prevents this.

**Python version mismatch.** Pydantic v2 requires Python 3.8+; some anthropic SDK features require 3.10+; the AI framework ecosystem largely targets 3.11+. Without a pinned version in `pyproject.toml`, a colleague on 3.9 will install the package but get subtle runtime errors on features that require newer syntax. `uv python pin 3.12` pins it once, for everyone.

**The "works in notebook, fails in production" trap.** A notebook that calls `load_dotenv()` in cell 3 but accesses `os.environ["OPENAI_API_KEY"]` in cell 1 works fine if you run cells in order in your session — but fails the first time someone runs it on a fresh kernel. Scripts with a single `load_dotenv()` at the top, tested with `uv run`, don't have this problem.

## Moving from setup to actual work

Once this environment is working, the next things to understand are what you are actually sending to the model and why it responds the way it does. The [tokenization article](/ai-engineering/articles/tokenization-bpe-intuition) explains how your text becomes the integer sequences the model sees, including why token counts matter for both cost and behavior. The [context window article](/ai-engineering/articles/context-windows-kv-cache-reality) covers what happens to everything you send — and what happens when you send too much.

For the big picture of where this toolkit fits in the AI engineer's actual job, the [AI Engineer role article](/ai-engineering/articles/ai-engineer-role-vs-ml-engineer) is worth reading next. Understanding what you are building toward — RAG pipelines, evaluation harnesses, agent orchestration — makes the environment choices here less arbitrary and more obviously right.

The [model landscape article](/ai-engineering/articles/model-landscape-2025-2026) covers which models to reach for once you have this baseline working. The short version: start with `gpt-4o-mini` or `claude-haiku-4` for exploration (cheap, fast, good enough to validate ideas), then move to a frontier model (`gpt-4o`, `claude-sonnet-4-5`) when you need quality. The environment you just set up calls both identically.

## The decision in practice

uv, `.env`, and five packages is the right starting point for 95% of AI engineering projects in 2026. The exceptions are narrow: if your project needs GPU inference locally, add conda or a separate CUDA management step. If your team already has a mature Poetry setup, the migration friction may not be worth it yet. If you are working in a corporate environment where package sources are restricted, you may need to point uv at an internal mirror with `UV_INDEX_URL`.

Everything else — the choice between LangChain and direct SDK calls, notebooks vs scripts, which frontier model to use — those are decisions you will make better after you have a working baseline. The setup is not the point. But it is the gate.

---

*Pricing figures are illustrative and based on publicly available rates as of mid-2026. Model provider pricing changes frequently — verify current rates in the provider's official documentation before estimating costs for a real project.*

## Frequently asked questions
Q: Should I use uv, Poetry, or conda for AI engineering in 2026?
A: uv is the right default for almost every AI engineering project in 2026. It installs packages 10-100x faster than pip, handles virtual environments and lockfiles in a single tool, and has no dependency on Rust toolchains or conda channels. Use conda only if you need non-Python native binaries (CUDA toolkit, cuDNN) that are not yet available via wheels; use Poetry if your team has already standardised on it and migration friction outweighs the speed gains.

Q: How should I store API keys for OpenAI and Anthropic in development?
A: Store API keys in a .env file at the project root, load them with python-dotenv or uv run --env-file .env, and add .env to your .gitignore immediately. Never hardcode keys in source files or pass them as CLI arguments (they appear in shell history). For CI/CD, inject keys as encrypted environment variables in your pipeline tool (GitHub Actions secrets, for example). Rotate keys immediately if one is ever committed to a repo.

Q: What is the minimal Python package set to start AI engineering?
A: For calling closed APIs: openai and anthropic. For loading environment variables: python-dotenv. For structured outputs and data validation: pydantic. For Jupyter notebooks: jupyterlab. That is four to five packages — resist the urge to install LangChain or LlamaIndex until you actually need orchestration; the abstractions add complexity before you understand what they are abstracting.

Q: When should I use Jupyter notebooks vs Python scripts for AI engineering work?
A: Use notebooks for exploration, one-off evaluations, and anything where you want to see intermediate outputs inline — token distributions, embedding visualizations, sample model responses. Use scripts (with a proper module structure) for anything that runs in CI, production, or as part of a pipeline. The anti-pattern is using a single sprawling notebook as both the exploration environment and the production code — that notebook will become unmaintainable within weeks.

Q: Does Python version matter for AI engineering?
A: Yes. Use Python 3.11 or 3.12. The openai and anthropic SDKs and pydantic v2 mostly support 3.9+, but the AI framework ecosystem (LangGraph, LlamaIndex) effectively targets 3.11+. Python 3.12 gives measurable interpreter speed gains (roughly 25% faster than 3.10 on compute-bound code) and is the version most CI images default to in 2026. Avoid 3.13+ for now — several GPU-adjacent packages (PyTorch, some CUDA wrappers) have not fully caught up.

Q: How do I structure a Python project for AI engineering work?
A: Start with a flat layout: src/ for source code, tests/ for tests, a pyproject.toml at the root managed by uv, and a .env file for secrets. Resist the temptation to create deep package hierarchies early — most AI projects pivot significantly in the first month. Add a Makefile with targets for install, lint, test, and run to keep the onboarding story simple for teammates.
