A hardcoded API key in source is a leak waiting to happen — committed to git, shared in screenshots, copied into chat threads. The fix is one of the oldest patterns in production code: read secrets from environment variables.
import os
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
raise RuntimeError("API_KEY env var not set")
print("API_KEY: present") # never print the value itselfWhy os.environ.get over os.environ[]?
os.environ.get("API_KEY") returns None when the variable isn't set — you handle the None yourself with a meaningful error. os.environ["API_KEY"] raises KeyError, which is less informative. Either works; .get + explicit check is clearer about intent.
And why never print the key?
Logs end up in lots of places — your terminal scrollback, log aggregators, error trackers, CI build outputs. Printing a secret means it's now in all of them. Print existence (API_KEY: present), print length (32 chars), print prefix-only (sk-abc...) if you must — never the full value.
Never hardcode secrets. API keys, tokens, passwords, signing secrets, database URLs with credentials — none of these belong in source code.
import os
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
raise RuntimeError("API_KEY env var not set")Two lines. The pattern is identical in every script that touches a credential.
| Environment | How you set it |
|---|---|
| Local dev | .env file loaded via python-dotenv, or export API_KEY=... && python script.py |
| Production (Vercel) | Project settings → Environment Variables |
| Production (any cloud) | platform-specific UI — Heroku config vars, AWS Parameter Store, etc. |
| CI | repository secrets (never committed; injected at run-time) |
# all safe to print:
print(f"API_KEY: {'present' if API_KEY else 'MISSING'}")
print(f"API_KEY length: {len(API_KEY)}")
print(f"API_KEY prefix: {API_KEY[:6]}...") # for tokens with sensible prefixes (sk-, pk_)
# never print:
print(API_KEY) # leaks the secret to logs
print(f"using {API_KEY}") # same# REQUIRED — must crash at startup if missing
API_KEY = os.environ["API_KEY"] # KeyError if missing — fail-fast
# REQUIRED, friendlier error
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
raise RuntimeError("API_KEY env var not set — see README")
# OPTIONAL — falls back to a default
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").env files for local dev# .env (NEVER commit this file)
API_KEY=sk-abc123...
DATABASE_URL=postgres://...
from dotenv import load_dotenv
load_dotenv() # reads .env into os.environ
import os
api_key = os.environ["API_KEY"]Add .env to .gitignore always. Add a .env.example with no real values to git so collaborators know which variables are required.
Besides direct prints, secrets leak through:
?api_key=sk-...)Sanitize what you log. For URLs with query-string secrets, log the path only:
from urllib.parse import urlparse
print(urlparse(url).path) # "/items" — no query stringA hardcoded API key in source is a leak waiting to happen — committed to git, shared in screenshots, copied into chat threads. The fix is one of the oldest patterns in production code: read secrets from environment variables.
import os
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
raise RuntimeError("API_KEY env var not set")
print("API_KEY: present") # never print the value itselfWhy os.environ.get over os.environ[]?
os.environ.get("API_KEY") returns None when the variable isn't set — you handle the None yourself with a meaningful error. os.environ["API_KEY"] raises KeyError, which is less informative. Either works; .get + explicit check is clearer about intent.
And why never print the key?
Logs end up in lots of places — your terminal scrollback, log aggregators, error trackers, CI build outputs. Printing a secret means it's now in all of them. Print existence (API_KEY: present), print length (32 chars), print prefix-only (sk-abc...) if you must — never the full value.
Never hardcode secrets. API keys, tokens, passwords, signing secrets, database URLs with credentials — none of these belong in source code.
import os
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
raise RuntimeError("API_KEY env var not set")Two lines. The pattern is identical in every script that touches a credential.
| Environment | How you set it |
|---|---|
| Local dev | .env file loaded via python-dotenv, or export API_KEY=... && python script.py |
| Production (Vercel) | Project settings → Environment Variables |
| Production (any cloud) | platform-specific UI — Heroku config vars, AWS Parameter Store, etc. |
| CI | repository secrets (never committed; injected at run-time) |
# all safe to print:
print(f"API_KEY: {'present' if API_KEY else 'MISSING'}")
print(f"API_KEY length: {len(API_KEY)}")
print(f"API_KEY prefix: {API_KEY[:6]}...") # for tokens with sensible prefixes (sk-, pk_)
# never print:
print(API_KEY) # leaks the secret to logs
print(f"using {API_KEY}") # same# REQUIRED — must crash at startup if missing
API_KEY = os.environ["API_KEY"] # KeyError if missing — fail-fast
# REQUIRED, friendlier error
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
raise RuntimeError("API_KEY env var not set — see README")
# OPTIONAL — falls back to a default
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").env files for local dev# .env (NEVER commit this file)
API_KEY=sk-abc123...
DATABASE_URL=postgres://...
from dotenv import load_dotenv
load_dotenv() # reads .env into os.environ
import os
api_key = os.environ["API_KEY"]Add .env to .gitignore always. Add a .env.example with no real values to git so collaborators know which variables are required.
Besides direct prints, secrets leak through:
?api_key=sk-...)Sanitize what you log. For URLs with query-string secrets, log the path only:
from urllib.parse import urlparse
print(urlparse(url).path) # "/items" — no query stringCreate a free account to get started. Paid plans unlock all tracks.