144. Requests + Os.Environ + Dotenv
HTTP API calls with requests and secrets from environment variables
144. Requests + Os.Environ + Dotenv
The requests Pattern
import os, requests
from dotenv import load_dotenv
load_dotenv()
def call_api(prompt: str) -> str:
api_key = os.environ.get('ANTHROPIC_API_KEY')
if not api_key:
raise EnvironmentError('ANTHROPIC_API_KEY not set')
response = requests.post(
'https://api.anthropic.com/v1/messages',
json={
'model': 'claude-haiku-4-5-20251001',
'max_tokens': 200,
'messages': [{'role': 'user', 'content': prompt}],
},
headers={
'x-api-key': api_key,
'anthropic-version': '2023-06-01',
},
timeout=30, # don't hang forever
)
response.raise_for_status()
return response.json()['content'][0]['text']With that background, Aryan wires up the LLM call:
def ask_llm(snapshot: dict, model: str) -> str:
api_key = os.environ.get('ANTHROPIC_API_KEY')
if not api_key:
raise EnvironmentError('ANTHROPIC_API_KEY not set')
pct = snapshot['percent']
top = snapshot['processes'][0]['name'] if snapshot['processes'] else 'none'
prompt = f"RAM at {pct:.1f}%. Top process: {top}. Give one brief suggestion."
response = requests.post(
'https://api.anthropic.com/v1/messages',
json={'model': model, 'max_tokens': 200,
'messages': [{'role': 'user', 'content': prompt}]},
headers={'x-api-key': api_key, 'anthropic-version': '2023-06-01'},
timeout=30,
)
response.raise_for_status()
return response.json()['content'][0]['text']He runs it. The LLM says: “Chrome is using 2.1 GB. Consider closing unused tabs or restarting the browser to reclaim memory.”
In forty lines of Python. Direct HTTP. No SDK. Just requests.
📺 YouTube search: “building with Claude API python tutorial” — you’ll be able to follow along start to finish after these exercises.
💡 Fun fact: The requests library was created by Kenneth Reitz in 2011 because Python’s built-in urllib required 7+ lines just to make a simple GET request. requests replaced all that with one line. It became one of the most downloaded Python packages ever — over 300 million downloads per month. The .env convention was popularized by the Twelve-Factor App methodology (2011), now the standard for cloud-native app configuration.
⚠️ Watch out: os.environ['KEY'] raises KeyError if the variable is missing — your whole script crashes with a confusing error. os.environ.get('KEY') returns None safely. Always use .get(), then check the result and raise a clear EnvironmentError with instructions on how to set the variable.
🤔 Think about it: Why should API keys never be committed to git, even in a private repository? What happens if you accidentally make the repo public later? What does raise_for_status() actually do — why is a 400 response not automatically an exception in requests?
🌐 Aryan wants to send the RAM snapshot to an LLM and get advice back. He hardcodes the API key in the source code, forgets to set Content-Type, and doesn’t call raise_for_status() so 4xx/5xx errors are silently swallowed.
Learning objectives
- Store API keys in .env, never in source code
- Load .env with load_dotenv() from python-dotenv
- Read env vars with os.environ.get() (safe, no KeyError)
- Raise descriptive EnvironmentError when key is missing
- Always call raise_for_status() to catch API errors
- Always set timeout= to prevent hanging
Key concepts
- .env file — secrets file excluded from git
- load_dotenv() — load .env into os.environ
- os.environ.get(‘KEY’) — safe read (None if missing)
- requests.post(json=, headers=, timeout=) — HTTP POST
- raise_for_status() — raise on 4xx/5xx
- HTTPError, Timeout, ConnectionError — exception types
Try it
Concept detail
HTTP API Calls with requests and dotenv
.env file — keep secrets out of source code
# .env (add to .gitignore!)
ANTHROPIC_API_KEY=sk-ant-api03-...
DATABASE_URL=postgresql://localhost/mydbfrom dotenv import load_dotenv
import os
load_dotenv() # reads .env into os.environ
api_key = os.environ.get('ANTHROPIC_API_KEY')
if not api_key:
raise EnvironmentError('ANTHROPIC_API_KEY not set')Making POST requests
import requests
response = requests.post(
'https://api.anthropic.com/v1/messages',
json={'model': 'claude-haiku-4-5-20251001', ...}, # auto sets Content-Type
headers={'x-api-key': api_key, 'anthropic-version': '2023-06-01'},
timeout=30
)
response.raise_for_status() # raise HTTPError on 4xx/5xx
data = response.json()Error handling
import requests
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
except requests.HTTPError as e:
print(f'API error {e.response.status_code}: {e.response.text}')
raise
except requests.Timeout:
print('Request timed out after 30s')
raise
except requests.ConnectionError:
print('No internet connection')
raise.gitignore — protect your secrets
.env
*.key
secrets/Solution
import os
import requests
from dotenv import load_dotenv
load_dotenv() # loads .env file into os.environ if present
API_URL = 'https://api.anthropic.com/v1/messages'
def load_api_key() -> str:
key = os.environ.get('ANTHROPIC_API_KEY')
if not key:
raise EnvironmentError(
'ANTHROPIC_API_KEY not set. Add it to .env or export it.'
)
return key
def ask_llm(prompt: str) -> str:
api_key = load_api_key()
payload = {
'model': 'claude-haiku-4-5-20251001',
'max_tokens': 256,
'messages': [{'role': 'user', 'content': prompt}],
}
headers = {
'x-api-key': api_key,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
}
response = requests.post(
API_URL, json=payload, headers=headers, timeout=30
)
response.raise_for_status() # raises HTTPError on 4xx/5xx
data = response.json()
return data['content'][0]['text']Tests
import os
import pytest
from unittest.mock import patch, MagicMock
def test_load_api_key_raises_when_missing():
with patch.dict(os.environ, {}, clear=True):
os.environ.pop('ANTHROPIC_API_KEY', None)
with pytest.raises(EnvironmentError):
load_api_key()
def test_load_api_key_returns_value():
with patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key-123'}):
key = load_api_key()
assert key == 'test-key-123'
def test_ask_llm_calls_raise_for_status():
import inspect
src = inspect.getsource(ask_llm)
assert 'raise_for_status' in src, 'Must call response.raise_for_status()'
def test_ask_llm_has_timeout():
import inspect
src = inspect.getsource(ask_llm)
assert 'timeout' in src, 'Must set a timeout on requests.post()'
def test_ask_llm_sends_correct_headers():
mock_response = MagicMock()
mock_response.json.return_value = {
'content': [{'text': 'Kill chrome to free RAM.'}]
}
mock_response.raise_for_status = MagicMock()
with patch('requests.post', return_value=mock_response) as mock_post:
with patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'}):
result = ask_llm('What should I do?')
_, kwargs = mock_post.call_args
headers = kwargs.get('headers', {})
assert 'x-api-key' in headers
assert 'anthropic-version' in headers
assert result == 'Kill chrome to free RAM.'
def test_api_key_not_hardcoded():
import inspect
src = inspect.getsource(ask_llm)
assert 'sk-ant' not in src, 'Never hardcode API keys in source code'