Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Python Environment Variables: os.environ, .env Files, and pydantic

Sean

Platform Writer

Jul 05, 2026
6 min read

Python environment variables are accessed via os.environ (stdlib) or os.environ.get() with defaults. The team that uses python-dotenv for local dev loads .env files automatically. The team that uses pydantic-settings has type-validated config. Hardcoded secrets in source code (API keys, DB passwords) are the #1 mistake; the team that uses env vars has secrets only in the environment, never in code or git.

Python Environment Variables: os.environ, .env Files, and pydantic

Table of contents

Reading env vars

import os

# Required - raises KeyError if missing
db_url = os.environ['DATABASE_URL']

# Optional with default
log_level = os.environ.get('LOG_LEVEL', 'INFO')

# Cast
port = int(os.environ.get('PORT', '8000'))
debug = os.environ.get('DEBUG', 'false').lower() == 'true'

The team that uses os.environ.get(..., default) always has working code (even with missing vars). The team that uses os.environ[...] crashes on missing vars (sometimes good, sometimes bad).

The .env file pattern

For local dev, use a .env file:

# .env (gitignored)
DATABASE_URL=postgresql://localhost/dev
SECRET_KEY=local-dev-only
DEBUG=true

Load it with python-dotenv:

from dotenv import load_dotenv
load_dotenv()  # loads .env into os.environ

The team that uses .env files has local config that doesn’t leak to git. The team that hardcodes config in code has ‘works on my machine’ issues.

pydantic-settings for validation

The team that wants type-safe config:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    secret_key: str
    log_level: str = 'INFO'
    debug: bool = False
    port: int = 8000

    class Config:
        env_file = '.env'

settings = Settings()  # raises ValidationError if required missing

The team that uses pydantic-settings gets validation at startup, not at first request. Errors surface immediately.

Production secrets

For production, don’t commit secrets anywhere:

  • Cloud: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault.
  • Container: Kubernetes Secrets (with encryption-at-rest) or external secret managers.
  • Bare metal: HashiCorp Vault, or environment injection from a secrets manager.

The team that uses Vault Agent has secrets pulled at runtime, never in code or disk. The team that uses AWS Secrets Manager + ECS task role has secrets in IAM, not env vars on disk.

Common pitfalls

  1. Booleans: env vars are strings. "false" == "false" is True, but os.environ.get('DEBUG') returns the string "false", not False. The team that casts value.lower() == 'true' has correct booleans.
  2. Numbers: PORT=8000 is a string "8000", not int. The team that casts int(...) has correct types.
  3. Whitespace: os.environ.get('TOKEN') may include trailing newlines from .env files. The team that uses .strip() has clean values.
  4. Empty strings: KEY= in .env gives empty string, not None. The team that checks if not value: handles empty correctly.

Twelve-factor app

The 12-factor app methodology says: store config in environment variables. The team that follows this has the same config in dev/staging/prod - just different env vars. No code changes between environments.

FAQ

What’s the difference between os.environ and os.getenv?

Same thing. os.getenv('VAR') is equivalent to os.environ.get('VAR'). The team that uses either has consistent style.

Should I commit .env files to git?

No. Add .env to .gitignore. The team that commits .env has secrets in git history (forever, even after removing).

How do I validate env vars are set?

Use pydantic-settings with required fields, or check at startup: required = ['DATABASE_URL', 'SECRET_KEY']; missing = [v for v in required if v not in os.environ]; assert not missing, .... The team that uses validation catches config errors early.

Can I use env vars for arrays or nested config?

Yes - JSON in env vars. ITEMS='[1,2,3]' then json.loads(os.environ['ITEMS']). The team that uses JSON for complex config has flexibility without nested env vars.

What about per-environment .env files?

.env.development, .env.production. Load the right one based on FLASK_ENV or DJANGO_SETTINGS_MODULE. The team that uses per-env files has separate config without env var gymnastics.

If you are sizing the infrastructure for the kind of project this post covers, the RunxBuild hosting calculator is the right place to model the line items. The compute, the memory, the storage, the bandwidth, the database - each one is a separate number, and the team’s mental model for the platform is the sum of those numbers. The RunxBuild dashboard is where the team sees the actual usage in one place.

Useful related references:

#python#env#config#dev-infra