API keys, secrets and environment variables
Most real APIs need a key to prove who you are. The single most important rule: never write a secret in your source code. A hardcoded key gets committed to git, pushed to GitHub, and is then effectively public forever — even if you delete it later, it's in the history.
The fix is environment variables: values set in the environment the program runs in, not in the code. You read them with the os module:
os.environ['WEATHER_KEY']— fetch the value; raisesKeyErrorif it's missing (loud, which is good).os.getenv('WEATHER_KEY')— returnsNoneif missing instead of raising; takes an optional default.
In development, keys usually live in a .env file that is listed in .gitignore so it's never committed; a tool like python-dotenv loads it into os.environ. In production they're set by the host (the server, container, or CI). Either way, the code only ever reads the name — os.environ['WEATHER_KEY'] — never the value itself.
The Rungo lesson uses a plain config dict as a stand-in for os.environ, so config[key_name] behaves exactly like os.environ[key_name] — including raising KeyError when the key is absent.
Syntax
import os
os.environ["WEATHER_KEY"] # value, or KeyError if missing
os.getenv("WEATHER_KEY") # value, or None if missing
os.getenv("PORT", "8000") # value, or a default
# NEVER:
api_key = "sk-live-9f3a..." # hardcoded secret — don't.Worked examples
Read a key from the environment (loud if missing)
import os
# os.environ behaves like a dict — missing key raises KeyError:
def get_api_key(env, key_name):
return env[key_name]
config = {'WEATHER_KEY': 'abc123'} # stands in for os.environ
print(get_api_key(config, 'WEATHER_KEY')) # abc123
# get_api_key(config, 'MISSING') → KeyError: 'MISSING'getenv with a sensible default
import os
# Optional config can have a fallback; secrets should NOT —
# a missing secret should crash loudly, not run with a default.
port = os.getenv('PORT', '8000')
print(f'Serving on port {port}')
# Output (when PORT is unset):
# Serving on port 8000Pass the key through to the call, never into source
def build_url(base, params):
parts = [f'{k}={v}' for k, v in sorted(params.items())]
return base + '?' + '&'.join(parts)
print(build_url('https://api.example.com/data',
{'city': 'London', 'units': 'metric'}))
# Output:
# https://api.example.com/data?city=London&units=metric
# The key would be loaded from os.environ and passed in params —
# it lives in the environment, never in this file.Common mistakes & gotchas
A secret in git is a leaked secret
Committing a key and 'removing it later' does NOT make it safe — it stays in the git history and anyone who clones the repo can read it. The only real fix is to rotate (revoke and reissue) the key. Keep secrets out of source from the start.
Don't give secrets a default
os.getenv('API_KEY', 'dev-key') quietly runs with a bogus key when the real one is missing, hiding a misconfiguration until it fails in production. For required secrets use os.environ['API_KEY'] so a missing value crashes loudly and early.
Don't print or log secrets
Dropping a key into a log line, an error message, or print() defeats the whole point — logs get stored, shipped to dashboards, and read by others. Treat the value as radioactive: read it, use it, never display it.
Why it matters
Leaked API keys are one of the most common — and most expensive — security incidents in the industry; bots scrape GitHub for them within minutes of a push. Loading secrets from the environment instead of source is a non-negotiable professional habit, and it's the line between a hobby script and code you can safely ship.