> ## Documentation Index
> Fetch the complete documentation index at: https://fastapi2day.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Pydantic Settings: Type-Safe Application Configuration

> Use BaseSettings to load and validate app configuration from environment variables and .env files with full type safety and secret handling.

Every application has configuration — a database URL, an API key, a port number, a feature flag. The conventional way to manage this in Python is `os.getenv()`, but it has a fundamental flaw: environment variables are always strings, and you have to manually parse them into the right types yourself. Miss a conversion and `PORT=8000` stays a string, or `DEBUG=False` evaluates to `True` because any non-empty string is truthy. Pydantic Settings solves this by bringing the same runtime validation you've been using for request data to your application's configuration — you declare what your settings look like, Pydantic reads the environment and validates everything for you, and your code works with typed, validated values from the start.

## The problem with raw environment variables

```python theme={null}
import os

# Set PORT=8000 in your environment:
port = os.getenv("PORT")
print(type(port))   # <class 'str'> — not an int!

server.run(port=port + 1)   # TypeError: can only concatenate str (not "int") to str

# Set DEBUG=False in your environment:
debug = os.getenv("DEBUG")
if debug:
    # This runs! "False" is a non-empty string — truthy in Python.
    print("Debug mode is active")
```

You end up writing the same boilerplate parsing code everywhere, and it's easy to get wrong.

<Warning>
  `os.getenv("DEBUG", "False")` returns the **string** `"False"`, not the boolean `False`. Always `== "True"` comparisons — or better yet, use Pydantic Settings to handle this automatically.
</Warning>

## Installation

`pydantic-settings` is a separate package (it ships separately from Pydantic v2):

```bash theme={null}
pip install pydantic-settings
```

```bash theme={null}
uv add pydantic-settings
```

## Basic usage

Inherit from `BaseSettings` and declare fields exactly as you would with `BaseModel`:

```python theme={null}
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    api_key: str
    port: int = 8000
    debug: bool = False

settings = Settings()

print(settings.port)    # 8000 (int, not str)
print(settings.debug)   # False (bool, not str)
```

When you instantiate `Settings()`, Pydantic automatically reads your environment variables (case-insensitive):

<Steps>
  <Step title="API_KEY">
    Required — no default. If not set, Pydantic raises a `ValidationError` immediately.
  </Step>

  <Step title="PORT">
    Optional — defaults to `8000`. If set, the string is automatically parsed to `int`.
  </Step>

  <Step title="DEBUG">
    Optional — defaults to `False`. Accepts `"True"`, `"False"`, `"1"`, `"0"`, `"yes"`, `"no"`.
  </Step>
</Steps>

## Reading from a `.env` file

In development, store your configuration in a `.env` file rather than setting shell variables by hand:

```bash theme={null}
# .env
API_KEY=sk_test_abc123
PORT=5000
DEBUG=True
```

Tell Pydantic Settings to read it with `SettingsConfigDict`:

```python theme={null}
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env")

    api_key: str
    port: int = 8000
    debug: bool = False

settings = Settings()

print(settings.api_key)   # sk_test_abc123
print(settings.port)      # 5000  (int)
print(settings.debug)     # True  (bool)
```

<Note>
  Environment variables set in your shell **always override** values in `.env`. This is intentional — your CI/CD pipeline or production host sets real secrets via environment variables, while `.env` provides convenient local defaults.
</Note>

## Using an environment prefix

If your host environment has many variables from different applications, prefix your settings to avoid collisions:

```bash theme={null}
# .env
MYAPP_API_KEY=sk_test_abc123
MYAPP_PORT=5000
```

```python theme={null}
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_prefix="myapp_",
    )

    api_key: str
    port: int = 8000

settings = Settings()
print(settings.api_key)   # sk_test_abc123  (read from MYAPP_API_KEY)
```

## Protecting secrets with `SecretStr`

Use `SecretStr` for sensitive values like passwords and API keys. Pydantic masks the value when the object is printed or logged, so secrets don't leak into your output:

```python theme={null}
from pydantic import SecretStr
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_password: SecretStr
    api_key: SecretStr

settings = Settings(database_password="super-secret", api_key="sk-abc123")

# Safe to log — value is masked
print(settings.database_password)
# SecretStr('**********')

# Access the real value only when you need it
print(settings.database_password.get_secret_value())
# super-secret
```

<Tip>
  Use `SecretStr` for any value you wouldn't want to appear in log files: API keys, database passwords, OAuth tokens, and similar credentials.
</Tip>

## Caching settings with `lru_cache`

Instantiating `Settings()` reads files and environment variables. Call it once and reuse the result everywhere via `lru_cache`:

```python theme={null}
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env")

    api_key: str
    port: int = 8000
    debug: bool = False

@lru_cache
def get_settings() -> Settings:
    return Settings()

# Call anywhere in your app — always returns the same instance
settings = get_settings()
print(settings.port)   # 8000
```

In FastAPI you'll typically inject `get_settings` as a dependency:

```python theme={null}
from fastapi import Depends, FastAPI

app = FastAPI()

@app.get("/info")
def info(settings: Settings = Depends(get_settings)):
    return {"port": settings.port, "debug": settings.debug}
```

## Environment-specific behaviour

Add computed properties to your settings class for environment-aware logic:

```python theme={null}
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env")

    environment: str = "development"
    debug: bool = False
    database_url: str = "sqlite:///dev.db"

    @property
    def is_production(self) -> bool:
        return self.environment == "production"

settings = Settings()

if settings.is_production:
    print("Running in production — extra safety checks active")
else:
    print(f"Running in {settings.environment} mode")
```

## A complete example

Here's a realistic settings class for a FastAPI application:

```python theme={null}
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict

class AppSettings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_prefix="app_")

    # Server
    host: str = "0.0.0.0"
    port: int = 8000
    debug: bool = False

    # Database
    database_url: str = "sqlite:///app.db"
    db_pool_size: int = 5

    # External API
    openai_api_key: SecretStr
    openai_model: str = "gpt-4o-mini"
    max_tokens: int = 1000

    @property
    def is_production(self) -> bool:
        return not self.debug

settings = AppSettings()

print(f"Server: {settings.host}:{settings.port}")
print(f"Model: {settings.openai_model}")
print(f"API key: {settings.openai_api_key}")   # SecretStr('**********')
```

And the corresponding `.env` file:

```bash theme={null}
APP_HOST=0.0.0.0
APP_PORT=8000
APP_DEBUG=False
APP_DATABASE_URL=postgresql://user:pass@localhost/mydb
APP_OPENAI_API_KEY=sk-abc123xyz
APP_OPENAI_MODEL=gpt-4o
```

<Accordion title="Why not just use a plain dict or config.py file?">
  A plain dictionary or module-level constants give you no validation — a typo in a variable name silently returns `None`, and environment variable parsing is manual and error-prone. `BaseSettings` gives you:

  * **Automatic type coercion** — strings become ints, bools, lists
  * **Validation errors** for missing required settings at startup, not at runtime
  * **Secret masking** via `SecretStr`
  * **Clear documentation** of every setting your app needs
  * **Overridable in tests** — you can pass values directly: `Settings(api_key="test-key")`
</Accordion>

## Learn more

* [Pydantic Settings documentation](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
* [pydantic-settings on GitHub](https://github.com/pydantic/pydantic-settings)

***

## What's next?

You now have a complete Pydantic toolkit — models, field constraints, nested structures, and settings. The next step is putting all of this to work inside a FastAPI application.

<Card title="Project Handling" icon="arrow-right" href="/practical-python/project-handling">
  Learn how to structure Python projects, manage dependencies, and package your applications.
</Card>
