Skip to content

API configuration using pydantic-settings#4

Merged
podmar merged 3 commits into
mainfrom
feat/config
May 31, 2026
Merged

API configuration using pydantic-settings#4
podmar merged 3 commits into
mainfrom
feat/config

Conversation

@podmar

@podmar podmar commented May 31, 2026

Copy link
Copy Markdown
Owner

What

Centralises app configuration using pydantic-settings.

Changes

  • Add config.py with a Settings class and get_settings() cached with lru_cache
  • DATABASE_URL now loaded via Settings
  • SecretStr used for database_url to prevent accidental logging
  • load_dotenv() removed from database.py — pydantic-settings handles .env loading
  • echo flag in async engine now driven by settings.environment

Why

Single source of truth for env vars, validation at startup, and a clean foundation for future config needs.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall Assessment

Excellent refactor! Moving to pydantic-settings is a solid choice for a FastAPI project. This pattern is widely used in production and will serve you well as the app grows.

What's working well:

✅ Using SecretStr for credentials shows good security awareness
✅ The @lru_cache optimization is exactly right for FastAPI dependencies
✅ Letting pydantic handle env validation removes boilerplate and improves error messages
✅ Clean separation of concerns (config in one place, database setup in another)

Main question:

The type: ignore[call-arg] comment suggests a type checker complaint. This might be a false positive from pyright — worth investigating whether it's actually needed.

Learning notes:

This PR demonstrates several FastAPI/Python best practices that are worth explaining in interviews:

  • Type-safe configuration (pydantic validates at startup, not at first use)
  • Dependency caching (lru_cache prevents re-reading env vars on every request)
  • Secret handling (SecretStr hides credentials in logs/errors)

The code is clean, focused, and ready to merge. Great work! 🚀

Comment thread backend/app/config.py
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
# no default value for database_url, it must be provided via environment variable or .env file
database_url: SecretStr = Field(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great use of SecretStr! This prevents the database URL (which contains credentials) from being accidentally logged or printed. When you call .get_secret_value() in database.py, you're explicitly acknowledging you need the raw value.

One thing to know: SecretStr doesn't encrypt anything — it just hides the value in logs and repr() output. The actual security comes from keeping the .env file out of version control (already done via .gitignore).

Comment thread backend/app/config.py


@lru_cache
def get_settings() -> Settings:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question about the type: ignore: This suggests pyright is complaining about calling Settings() with no arguments.

In my experience, this shouldn't be necessary — BaseSettings is designed to work without arguments (it loads from env vars automatically). A few things to try:

  1. Check if removing the comment triggers an actual error
  2. If pyright still complains, it might be a false positive due to how pydantic_settings uses model_config

If you do need the ignore, consider making it more specific: type: ignore[call-arg] is fine, but a brief inline comment explaining why would help future maintainers (including you in 6 months!).

Comment thread backend/app/config.py
environment: str = "development"


@lru_cache

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of @lru_cache! This ensures Settings() is only instantiated once, even if get_settings() is called many times.

For context: FastAPI dependencies like this are called on every request, so without caching you'd be re-reading env vars constantly. The cache makes this essentially free after the first call.

One small note: in Python 3.9+, you can use @lru_cache without parentheses (it's equivalent to @lru_cache(maxsize=128)), but @lru_cache() is totally fine and arguably clearer about intent.

Comment thread backend/app/database.py
"DATABASE_URL environment variable is required. "
"See .env.example for setup instructions."
)
settings = get_settings()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good refactor here! Moving from manual dotenv.load_dotenv() + os.environ.get() to pydantic-settings gives you:

  1. Type safety — Settings fields are validated at startup
  2. Better error messages — If DATABASE_URL is missing, pydantic will tell you exactly what's wrong
  3. Testability — You can pass a custom Settings object in tests
  4. Less boilerplate — No need to check if not DATABASE_URL

The old explicit check was good defensive coding, but pydantic's validation is more comprehensive (it also checks types, not just presence).

Comment thread backend/app/config.py
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
# no default value for database_url, it must be provided via environment variable or .env file
database_url: SecretStr = Field(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future enhancement idea (not blocking): If you want to validate that database_url actually looks like a PostgreSQL URL, you could use pydantic's PostgresDsn type:

from pydantic import PostgresDsn

class Settings(BaseSettings):
    database_url: PostgresDsn = Field(...)
    # ...

This would catch typos like postgres:// vs postgresql:// at startup. But SecretStr works fine too — just a tradeoff between stricter validation vs. keeping credentials hidden in error messages.

@claude

claude Bot commented May 31, 2026

Copy link
Copy Markdown

💰 Review cost: $0.15

@podmar
podmar merged commit 6162a38 into main May 31, 2026
@podmar
podmar deleted the feat/config branch May 31, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant