Why Environment Setup Matters for Agent Work
Agent development has higher setup stakes than typical Python work because:
- Multiple expensive API keys need to stay out of source control
- Async code is the norm, sync-only setups break at scale
- Reproducibility is critical, model behavior can shift with library version changes
- Local observability from day one saves hours of debugging later
This lesson gets you from zero to a working, professional setup in under 20 minutes.
Package Manager: Why uv
The Python ecosystem has settled on uv as the standard for new projects. Install it once:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# Verify
uv --version # uv 0.5.x
Create and activate a new project:
uv init agent-project
cd agent-project
# uv creates .venv automatically on first use: no separate python -m venv step needed
uv venv --python 3.12
source .venv/bin/activate # macOS/Linux
# .venv\Scripts\activate # Windows
Project Structure
Follow this layout for every agent project. It scales from a weekend hack to a production service:
agent-project/
├── pyproject.toml # Dependencies and project metadata
├── uv.lock # Locked dependency tree (commit this)
├── .env # API keys, NEVER commit
├── .env.example # Template with key names, no values (commit this)
├── .gitignore # Include .env and .venv
├── src/
│ └── agents/
│ ├── __init__.py
│ ├── tools.py # Tool definitions
│ ├── graph.py # LangGraph agent graph
│ └── config.py # Settings via pydantic-settings
├── tests/
│ ├── test_tools.py
│ └── test_graph.py
└── scripts/
└── run_agent.py # Entry point for local runs
Installing Core Dependencies
The pyproject.toml is the single source of truth for your project’s dependencies. Pin minimum versions to avoid breaking changes from upstream libraries.
# pyproject.toml
[project]
name = "agent-project"
version = "0.1.0"
requires-python = ">=3.12" # 3.12+ required for modern typing features used by Pydantic AI and LangGraph
dependencies = [
# LLM providers
"openai>=1.50.0", # 1.50+ includes the Assistants v2 API and structured output mode
"anthropic>=0.40.0", # 0.40+ includes streaming tool use and the Messages API
"google-generativeai>=0.8.0",
# Agent frameworks
"langgraph>=0.2.0", # 0.2+ is the stable graph API, earlier versions had breaking changes
"langchain-core>=0.3.0", # core abstractions shared by LangGraph and LangChain tools
"pydantic-ai>=0.0.14",
# Memory & retrieval
"langchain-openai>=0.2.0", # OpenAI embeddings for vector search
"qdrant-client>=1.11.0", # vector database client for semantic memory
# Configuration & validation
"pydantic>=2.9.0", # Pydantic v2 is required, v1 is incompatible with these frameworks
"pydantic-settings>=2.6.0", # reads .env files and validates types at startup
"python-dotenv>=1.0.0",
# Observability
"langsmith>=0.1.130", # LangSmith tracing, auto-instruments LangGraph runs with zero config
# HTTP & async
"httpx>=0.27.0", # async HTTP client, prefer over requests for agent tool implementations
"aiohttp>=3.10.0",
"tenacity>=9.0.0", # retries, agents hitting flaky APIs need exponential backoff
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.24.0", # required to run async def test_ functions
"pytest-mock>=3.14.0",
"ruff>=0.7.0", # replaces black + flake8 + isort in a single fast tool
]
Install everything:
uv sync # installs all dependencies from pyproject.toml and uv.lock
uv sync --extra dev # also installs the [dev] optional group (pytest, ruff, etc.)
API Key Management
Step 1: Create your .env file (never commit this):
# .env: local secrets, excluded from git
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=AIza...
LANGSMITH_API_KEY=lsv2_...
LANGSMITH_TRACING=true # set to false to disable tracing and avoid quota usage
LANGSMITH_PROJECT=my-agent-project # groups runs in the LangSmith UI
Step 2: Create .env.example (commit this as documentation):
# .env.example: copy to .env and fill in your keys
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GOOGLE_API_KEY=
LANGSMITH_API_KEY=
LANGSMITH_TRACING=true
LANGSMITH_PROJECT=
Step 3: Add to .gitignore:
echo ".env" >> .gitignore # prevents accidental key commits
echo ".venv/" >> .gitignore # keeps the virtual environment out of the repo
Step 4: Load in Python via pydantic-settings (preferred over raw dotenv):
# src/agents/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# SettingsConfigDict tells pydantic-settings where to find the .env file
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
# Required fields: startup raises ValidationError if these are missing from .env
openai_api_key: str
anthropic_api_key: str
langsmith_api_key: str = "" # optional: empty string disables tracing
langsmith_tracing: bool = False
langsmith_project: str = "default"
# Agent defaults: centralizing these here makes it easy to tune without touching agent code
default_model: str = "gpt-5.6-luna"
max_iterations: int = 20 # hard cap on ReAct loop iterations, prevents runaway agents
temperature: float = 0.0 # 0.0 = deterministic outputs, important for tool calling reliability
# Instantiated once at import time: all modules import this singleton
settings = Settings()
pydantic-settings validates types at startup, you’ll get a clear error if a required key is missing, instead of a cryptic None crash three layers deep.
Verify Your Setup: First Agent Run
Test that everything works end to end with this minimal script:
# scripts/run_agent.py
import asyncio
from openai import AsyncOpenAI # async client: use this inside async def functions
from anthropic import AsyncAnthropic
from src.agents.config import settings
async def test_openai():
client = AsyncOpenAI(api_key=settings.openai_api_key)
response = await client.chat.completions.create(
model="gpt-5.6-luna",
messages=[{"role": "user", "content": "Reply with just: OpenAI OK"}],
max_tokens=20, # small limit keeps this verification call cheap
)
print(response.choices[0].message.content)
async def test_anthropic():
client = AsyncAnthropic(api_key=settings.anthropic_api_key)
response = await client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=20,
messages=[{"role": "user", "content": "Reply with just: Anthropic OK"}],
)
# Anthropic returns a list of content blocks; [0].text extracts the text from the first block
print(response.content[0].text)
async def main():
print("Testing OpenAI...")
await test_openai()
print("Testing Anthropic...")
await test_anthropic()
print("\nSetup complete!")
if __name__ == "__main__":
asyncio.run(main()) # asyncio.run() is the standard entry point for async scripts
Run it:
uv run python scripts/run_agent.py
# OpenAI OK
# Anthropic OK
# Setup complete!
Setting Up LangSmith Tracing
LangSmith is Anthropic-agnostic, it traces any LangChain or LangGraph execution. Enabling it is two lines:
# At the top of any entry point script: must be set before importing LangGraph
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = settings.langsmith_api_key
# Now any LangGraph or LangChain run automatically sends traces to
# https://smith.langchain.com/projects/my-agent-project
# No other code changes needed: LangGraph instruments itself via the env vars
With tracing on, every agent run appears in the LangSmith UI with a complete breakdown of each LLM call: input, output, latency, tokens, and cost. This becomes indispensable the moment your agent breaks in a non-obvious way.
Quick Dev Workflow Tips
Use ruff for fast linting:
uv run ruff check src/ # lint, catches unused imports, undefined names, style issues
uv run ruff format src/ # format (replaces black)
Run async tests with pytest-asyncio:
# tests/test_tools.py
import pytest
# @pytest.mark.asyncio tells pytest-asyncio to run this coroutine in an event loop
@pytest.mark.asyncio
async def test_my_tool():
result = await my_tool("test input")
assert result is not None
uv run pytest tests/ -v
Hot-reload during development: For FastAPI agent servers, use uvicorn --reload. For LangGraph scripts, just re-run the script, state is managed explicitly, so reruns are clean.
You now have a professional Python agent workspace. The next lessons build the actual agent logic on top of this foundation.