Use Environment Variables
What Environment Variables Are Used For
An environment variable is a named text value that the operating system passes to a program when it starts. Lesson 7 used one, ANTHROPIC_API_KEY, to give the SDK your API key. This lesson goes further. It covers how to read environment variables in Python, which ones the SDK understands, how to load them from a .env file during development, and how to organize them into a clean configuration layer for a real application.
Why Applications Use Environment Variables
A program needs two kinds of information:
- Code: the logic, which is the same everywhere.
- Configuration: values that change between environments, such as API keys, model names, timeouts, and feature switches.
Keeping configuration out of the code and in the environment has clear benefits:
| Benefit | Explanation |
|---|---|
| Security | Secrets never appear in source code or Git history |
| One codebase, many environments | Development, staging, and production run identical code with different settings |
| No code change for configuration changes | Switching the model or rotating a key means changing a variable and restarting, not editing and redeploying code |
| Platform support | Docker, CI systems, hosting platforms, and servers all support environment variables natively |
This separation of configuration from code is one of the principles of the widely used Twelve-Factor App methodology.
Reading Environment Variables in Python
Python exposes the process environment through os.environ, a dictionary-like object whose keys and values are strings.
os.environ["NAME"]: Required Values
import os
api_key = os.environ["ANTHROPIC_API_KEY"]
If the variable is not set, this raises KeyError. Use it for values the program cannot run without. The failure happens immediately and points directly at the missing variable.
os.environ.get("NAME", default): Optional Values
import os
model = os.environ.get("CLAUDE_MODEL", "claude-opus-5-5")
If the variable is missing, .get() returns the default, or None if you give no default. Use it for settings with a sensible fallback.
os.getenv("NAME", default) is equivalent to os.environ.get(...).
Everything Is a String
Environment variables always arrive as text. Numbers and booleans must be converted explicitly:
import os
max_tokens = int(os.environ.get("CLAUDE_MAX_TOKENS", "1024"))
timeout = float(os.environ.get("CLAUDE_TIMEOUT", "60"))
Booleans need special care:
import os
# Wrong: any non-empty string is truthy, so "false" becomes True
debug = bool(os.environ.get("APP_DEBUG", "false"))
# Correct: compare the text explicitly
debug = os.environ.get("APP_DEBUG", "false").lower() in {"1", "true", "yes", "on"}
Why the first version is wrong: bool("false") is True, because Python treats every non-empty string as true. Only an empty string is false.
| Expression | Missing variable | Use for |
|---|---|---|
os.environ["X"] | Raises KeyError | Required settings, such as API keys |
os.environ.get("X") | Returns None | Optional settings, checked later |
os.environ.get("X", "default") | Returns "default" | Optional settings with a fallback |
Environment Variables the SDK Reads
The Anthropic SDK reads several variables automatically:
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY | API key used when no credential is passed in code (Lesson 7) |
ANTHROPIC_AUTH_TOKEN | Bearer token, used when no API key is found |
ANTHROPIC_BASE_URL | Overrides the API address, for example to route through a gateway or test server |
ANTHROPIC_LOG | Enables SDK logging: set to info or debug |
ANTHROPIC_PROFILE | Selects a named credential profile |
Cloud platform clients read their own variables as well, such as ANTHROPIC_AWS_WORKSPACE_ID for AnthropicAWS. Lessons 161–170 cover them.
Example: Turning On SDK Logging
ANTHROPIC_LOG is useful when you need to see what the SDK is doing, such as retries, request timing, and HTTP details:
# macOS / Linux
ANTHROPIC_LOG=debug python first_look.py
# Windows PowerShell
$env:ANTHROPIC_LOG = "debug"
python first_look.py
The VAR=value command syntax in bash and zsh sets the variable only for that one command, which is ideal for a quick debugging run.
Warning: Debug logs include detailed request information. Use them during development, and do not leave debug logging enabled in production, where logs are stored and shared more widely.
Your Own Application Variables
The SDK does not read variables such as CLAUDE_MODEL or CLAUDE_MAX_TOKENS. Those are your application's settings, which your code reads and passes to the SDK. A consistent prefix, such as CLAUDE_ or your app's name, keeps them easy to find and avoids clashes with other tools.
.env Files for Local Development
Setting several variables in every new terminal quickly becomes tedious. A .env file stores them in your project folder:
# .env: local development settings (never commit this file)
ANTHROPIC_API_KEY=sk-ant-api03-...
CLAUDE_MODEL=claude-opus-5-5
CLAUDE_MAX_TOKENS=1024
APP_DEBUG=true
Python does not read .env files on its own. The widely used python-dotenv library loads them into os.environ.
Install python-dotenv
python -m pip install python-dotenv
The package name is python-dotenv, but you import it as dotenv.
Load the File
from dotenv import load_dotenv
load_dotenv() # Reads .env and adds its values to os.environ
from anthropic import Anthropic
client = Anthropic() # Now finds ANTHROPIC_API_KEY
How load_dotenv() works:
- It looks for a file named
.env. With no path argument, it searches starting from the directory of the script that calls it and moves up through parent directories. - It parses each
KEY=valueline. - It sets each value in
os.environ, unless that variable is already set.
Real Variables Win by Default
Point 3 is important. By default, load_dotenv() does not override variables that already exist in the environment:
load_dotenv() # Existing environment variables take precedence
load_dotenv(override=True) # Values from .env replace existing ones
Why this default is useful: on a server, the real environment variables, set by the platform, take precedence, and a stray .env file cannot accidentally replace production settings. During development, the .env file fills in whatever your shell has not set.
Use an Explicit Path for Reliability
The automatic search can find the wrong file, or none, when scripts run from different folders, from schedulers, or inside notebooks. An explicit path removes the guesswork:
from pathlib import Path
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")
Path(__file__)is the path of the current Python file..resolve()turns it into an absolute path..parentis the folder containing it./ ".env"joins the file name to that folder.
The .env file is now always found next to the script, regardless of the current working directory.
Read Values Without Touching os.environ
dotenv_values() returns the file's contents as a dictionary and leaves the environment unchanged. This is useful for inspecting or merging configuration:
from dotenv import dotenv_values
config = dotenv_values(".env")
print(config["CLAUDE_MODEL"]) # claude-opus-5-5
.env File Syntax
# Comments start with #
ANTHROPIC_API_KEY=sk-ant-api03-...
# Quotes are optional; use them when the value contains spaces
APP_NAME="Support Assistant"
# Empty value
OPTIONAL_SETTING=
Keep each variable on its own line, and avoid spaces around =. That is the most portable format, and Docker's --env-file option also reads it.
Protecting .env Files
Never Commit .env
Add it to .gitignore before creating it:
.env
.venv/
Check that Git ignores it:
git check-ignore .env
If the command prints .env, the file is ignored. If it prints nothing, it is not ignored. Fix .gitignore before committing anything.
Commit a .env.example Instead
A .env.example file documents which variables the project needs, with no real secrets:
# .env.example: copy to .env and fill in real values
ANTHROPIC_API_KEY=
CLAUDE_MODEL=claude-opus-5-5
CLAUDE_MAX_TOKENS=1024
APP_DEBUG=false
A new developer copies it and fills in their own key:
cp .env.example .env
This keeps the list of required settings in version control while the secrets stay out of it.
Note:
.envfiles are a development convenience. On servers and in CI, set environment variables through the platform, such as systemd, Docker, a CI secrets store, or the hosting dashboard, or use a secrets manager. Do not copy.envfiles to production servers.
Practical Example: A Configuration Module
Reading os.environ throughout an application scatters configuration logic everywhere. A better design reads and validates all settings once, at startup, in one module.
Project Structure
claude-tutorial/
├── .env # Local secrets (ignored by Git)
├── .env.example # Template (committed)
├── .gitignore
├── config.py # Loads and validates settings
└── main.py # Uses the settings
config.py
"""config.py: load and validate application settings from the environment."""
import os
from dataclasses import dataclass
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parent / ".env")
class ConfigError(RuntimeError):
"""Raised when a required setting is missing or invalid."""
def _require(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value:
raise ConfigError(f"Missing required environment variable: {name}")
return value
def _get_int(name: str, default: int, minimum: int = 1) -> int:
raw = os.environ.get(name, str(default)).strip()
try:
value = int(raw)
except ValueError:
raise ConfigError(f"{name} must be an integer, got {raw!r}") from None
if value < minimum:
raise ConfigError(f"{name} must be at least {minimum}, got {value}")
return value
def _get_bool(name: str, default: bool = False) -> bool:
raw = os.environ.get(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
@dataclass(frozen=True)
class Settings:
anthropic_api_key: str
model: str
max_tokens: int
debug: bool
def load_settings() -> Settings:
api_key = _require("ANTHROPIC_API_KEY")
if not api_key.startswith("sk-ant-"):
raise ConfigError("ANTHROPIC_API_KEY does not look like a Claude API key")
return Settings(
anthropic_api_key=api_key,
model=os.environ.get("CLAUDE_MODEL", "claude-opus-5-5").strip(),
max_tokens=_get_int("CLAUDE_MAX_TOKENS", default=1024),
debug=_get_bool("APP_DEBUG"),
)
settings = load_settings()
main.py
"""main.py: use validated settings to call Claude."""
from anthropic import Anthropic
from config import settings
client = Anthropic(api_key=settings.anthropic_api_key)
message = client.messages.create(
model=settings.model,
max_tokens=settings.max_tokens,
messages=[{"role": "user", "content": "Give me one tip for writing clean Python."}],
)
answer = "".join(block.text for block in message.content if block.type == "text")
print(answer)
if settings.debug:
print(f"\n[debug] model={message.model} usage={message.usage}")
How It Works
load_dotenv(...)runs whenconfig.pyis imported. Any module that importssettingsis guaranteed that the.envvalues have already been loaded. Because the default isoverride=False, real environment variables on a server still take precedence._require()treats a missing or empty value as an error. It also calls.strip()to remove stray whitespace or newlines from copy-pasting._get_int()converts text to an integer and validates it. A typo such asCLAUDE_MAX_TOKENS=1o24produces a clearConfigErrornaming the variable, instead of a confusing failure deep inside a request.from Nonehides the less helpful internalValueErrortraceback._get_bool()parses booleans explicitly, avoiding thebool("false") == Truetrap.Settingsis a frozen dataclass. Each setting has a name and a type that editors can autocomplete, andfrozen=Trueprevents code from accidentally changing configuration at runtime.settings = load_settings()runs once, at import. If anything is wrong, the application fails at startup with a precise message, not on the first user request.main.pynever touchesos.environ. It depends only onsettings, which makes it easier to read and to test.
Example output when the key is missing:
config.ConfigError: Missing required environment variable: ANTHROPIC_API_KEY
Switching Configuration Without Changing Code
Because the model comes from the environment, you can try a different model for a single run:
CLAUDE_MODEL=claude-sonnet-5-5 python main.py
The value set on the command line overrides the .env file, because load_dotenv() does not override variables that already exist.
Environment-Specific Configuration
Real projects run in several environments, each with its own values:
| Variable | Development | Staging | Production |
|---|---|---|---|
ANTHROPIC_API_KEY | Personal key | Service account key (staging workspace) | Service account key (production workspace) |
CLAUDE_MODEL | Possibly a cheaper model for experiments | Same as production | The tested production model |
APP_DEBUG | true | false | false |
ANTHROPIC_LOG | debug when needed | unset | unset |
| Where values live | .env file | CI or platform secrets | Platform secrets or secrets manager |
The application code is identical in all three. Only the environment differs.
Django Example
In Django, read environment variables in settings.py, which runs once at startup:
# settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / ".env")
ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]
CLAUDE_MODEL = os.environ.get("CLAUDE_MODEL", "claude-opus-5-5")
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"
Other modules then use from django.conf import settings and settings.ANTHROPIC_API_KEY, as shown in Lesson 7.
Common Mistakes
Loading .env After Creating the Client
from anthropic import Anthropic
from dotenv import load_dotenv
client = Anthropic() # ANTHROPIC_API_KEY is not set yet
load_dotenv() # Too late for the client above
Why it fails: the client looks up its credential when it is created. Loading the file afterwards does not affect a client that already exists.
Fix: call load_dotenv() first, ideally at the top of your entry point or in a configuration module that is imported first.
Committing .env
What happens: your API key ends up in the repository, and anyone with access, or the whole internet for a public repository, can use it.
Fix: add .env to .gitignore before creating it, and verify with git check-ignore .env. If a key was committed, revoke it in the Console immediately (Lesson 6).
Expecting .env to Override Existing Variables
What happens: you change the model in .env, but the program keeps using the old value.
Why: the variable is also set in your shell or system environment, and load_dotenv() does not override existing variables by default.
Fix: remove the shell variable (unset CLAUDE_MODEL), or use load_dotenv(override=True) deliberately in development.
Treating "false" as False
Fix: parse booleans explicitly, as _get_bool() does.
The .env File Is Not Found
What happens: everything works when you run the script from the project folder, but fails when it runs from another directory, a scheduler, or a notebook.
Fix: pass an explicit path: load_dotenv(Path(__file__).resolve().parent / ".env").
Scattering os.environ Calls Throughout the Code
What happens: configuration is read in dozens of places, defaults differ between them, and missing values cause errors at unpredictable times.
Fix: centralize all configuration in one module that validates everything at startup.
Best Practices
- Separate configuration from code. Secrets, model names, limits, and feature switches belong in the environment.
- Use
os.environ["X"]for required values and.get("X", default)for optional ones. - Convert and validate types explicitly, especially integers and booleans.
- Centralize configuration in one module, and fail fast at startup when something is wrong.
- Use
.envfiles only for local development, always ignored by Git, with a committed.env.example. - Use platform secrets or a secrets manager in production.
- Load environment files before creating SDK clients.
- Prefix your own variables (
CLAUDE_,APP_) so they are easy to identify. - Never log secret values. Log only masked versions.
Exercise
- Create a project with
.env,.env.example,.gitignore,config.py, andmain.pyas shown above. - Confirm that
git check-ignore .envprints.env. - Run
main.py. Then runCLAUDE_MODEL=claude-sonnet-5-5 python main.py(or the PowerShell equivalent) withAPP_DEBUG=true, and confirm that the debug line shows the different model. - Set
CLAUDE_MAX_TOKENS=abcin.envand run again. What error do you get, and when does it occur? - Add a new optional setting,
CLAUDE_TIMEOUT(a float, default60.0), toSettings, and pass it to the client astimeout=.
config.ConfigError: CLAUDE_MAX_TOKENS must be an integer, got 'abc'. It occurs at startup, whenconfig.pyis imported, before any request is sent.Add a float helper and a field:
def _get_float(name: str, default: float) -> float:
raw = os.environ.get(name, str(default)).strip()
try:
return float(raw)
except ValueError:
raise ConfigError(f"{name} must be a number, got {raw!r}") from None
@dataclass(frozen=True)
class Settings:
anthropic_api_key: str
model: str
max_tokens: int
debug: bool
timeout: float
Add timeout=_get_float("CLAUDE_TIMEOUT", 60.0) to load_settings(), then create the client with:
client = Anthropic(api_key=settings.anthropic_api_key, timeout=settings.timeout)