DEV Community

Stack Horizon
Stack Horizon

Posted on

Environment Config the Clean Way

The Problem with Environment Variables

We all know the drill. You clone a repo, create a .env file, and start coding. But soon the config grows messy. You have variables for dev, staging, production. Some are optional, some required. Your code is littered with os.getenv('SOME_VAR') and fallback logic. It's fragile and hard to maintain.

Let me show you a cleaner approach that I've used across several projects.

The Core Idea: A Config Object

Instead of scattering os.getenv calls everywhere, centralize all configuration into a single object. This object validates required variables, provides defaults, and gives you a single source of truth.

Here's a Python example using pydantic (but the concept applies to any language):

from pydantic import BaseSettings, Field

class Settings(BaseSettings):
    app_name: str = Field("MyApp", env="APP_NAME")
    debug: bool = Field(False, env="DEBUG")
    database_url: str = Field(..., env="DATABASE_URL")
    secret_key: str = Field(..., env="SECRET_KEY")
    port: int = Field(8000, env="PORT")

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

settings = Settings()
Enter fullscreen mode Exit fullscreen mode

Now you access settings.database_url instead of os.getenv('DATABASE_URL'). Clean and type-safe.

Handling Multiple Environments

You can extend this pattern for different environments. Create a base config and inherit:

class BaseConfig(BaseSettings):
    database_url: str = Field(..., env="DATABASE_URL")
    secret_key: str = Field(..., env="SECRET_KEY")

class DevConfig(BaseConfig):
    debug: bool = True
    database_url: str = "sqlite:///dev.db"

class ProdConfig(BaseConfig):
    debug: bool = False
    database_url: str = Field(..., env="PROD_DATABASE_URL")
Enter fullscreen mode Exit fullscreen mode

Then load the right one based on an environment variable:

import os

env = os.getenv("ENV", "dev")
if env == "prod":
    settings = ProdConfig()
else:
    settings = DevConfig()
Enter fullscreen mode Exit fullscreen mode

Validation and Errors

One of the biggest wins is early validation. If DATABASE_URL is missing, your app fails immediately with a clear error message, not a cryptic database connection timeout later.

try:
    settings = Settings()
except Exception as e:
    print(f"Configuration error: {e}")
    sys.exit(1)
Enter fullscreen mode Exit fullscreen mode

Keep Secrets Out of Code

Never hardcode secrets. Use .env files locally, and real secrets from your CI/CD pipeline or secret manager in production. The config object reads from environment variables, so you can inject them however you like.

What About Other Languages?

  • Node.js: Use env-var or convict for validation.
  • Go: Use envconfig or viper.
  • Rust: Use config crate with dotenv.

The pattern is the same: parse everything once, validate, and use a typed config object.

The Clean Way Checklist

  1. Centralize all config in one module.
  2. Validate required fields at startup.
  3. Provide sensible defaults for non-critical vars.
  4. Use typed fields to avoid stringly-typed code.
  5. Never access os.getenv outside the config module.

Final Thoughts

This approach has saved me countless hours of debugging misconfigured environments. It's simple, testable, and scales from small scripts to large applications. Give it a try in your next project.

Happy coding!

Top comments (0)