← Home

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/mydb
from 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'

Resources