Use Environment Variables

Su Suriya Ravichandran Updated 10 Nov 2026
12 min read ·Lesson 8 of 200

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:

BenefitExplanation
SecuritySecrets never appear in source code or Git history
One codebase, many environmentsDevelopment, staging, and production run identical code with different settings
No code change for configuration changesSwitching the model or rotating a key means changing a variable and restarting, not editing and redeploying code
Platform supportDocker, 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.

ExpressionMissing variableUse for
os.environ["X"]Raises KeyErrorRequired settings, such as API keys
os.environ.get("X")Returns NoneOptional 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:

VariablePurpose
ANTHROPIC_API_KEYAPI key used when no credential is passed in code (Lesson 7)
ANTHROPIC_AUTH_TOKENBearer token, used when no API key is found
ANTHROPIC_BASE_URLOverrides the API address, for example to route through a gateway or test server
ANTHROPIC_LOGEnables SDK logging: set to info or debug
ANTHROPIC_PROFILESelects 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:

  1. 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.
  2. It parses each KEY=value line.
  3. 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.
  • .parent is 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: .env files 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 .env files 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

  1. load_dotenv(...) runs when config.py is imported. Any module that imports settings is guaranteed that the .env values have already been loaded. Because the default is override=False, real environment variables on a server still take precedence.
  2. _require() treats a missing or empty value as an error. It also calls .strip() to remove stray whitespace or newlines from copy-pasting.
  3. _get_int() converts text to an integer and validates it. A typo such as CLAUDE_MAX_TOKENS=1o24 produces a clear ConfigError naming the variable, instead of a confusing failure deep inside a request. from None hides the less helpful internal ValueError traceback.
  4. _get_bool() parses booleans explicitly, avoiding the bool("false") == True trap.
  5. Settings is a frozen dataclass. Each setting has a name and a type that editors can autocomplete, and frozen=True prevents code from accidentally changing configuration at runtime.
  6. 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.
  7. main.py never touches os.environ. It depends only on settings, 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:

VariableDevelopmentStagingProduction
ANTHROPIC_API_KEYPersonal keyService account key (staging workspace)Service account key (production workspace)
CLAUDE_MODELPossibly a cheaper model for experimentsSame as productionThe tested production model
APP_DEBUGtruefalsefalse
ANTHROPIC_LOGdebug when neededunsetunset
Where values live.env fileCI or platform secretsPlatform 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 .env files 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

  1. Create a project with .env, .env.example, .gitignore, config.py, and main.py as shown above.
  2. Confirm that git check-ignore .env prints .env.
  3. Run main.py. Then run CLAUDE_MODEL=claude-sonnet-5-5 python main.py (or the PowerShell equivalent) with APP_DEBUG=true, and confirm that the debug line shows the different model.
  4. Set CLAUDE_MAX_TOKENS=abc in .env and run again. What error do you get, and when does it occur?
  5. Add a new optional setting, CLAUDE_TIMEOUT (a float, default 60.0), to Settings, and pass it to the client as timeout=.
Show answers for steps 4 and 5
  1. config.ConfigError: CLAUDE_MAX_TOKENS must be an integer, got 'abc'. It occurs at startup, when config.py is imported, before any request is sent.

  2. 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)

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Use Environment Variables and get answers drawn from it.

Signed-in readers only.