Repository: pydantic/pydantic

Issue:
Allow configuring env letter case rules for settings parser
Issue derived from https://github.com/samuelcolvin/pydantic/issues/275

Currently `BaseSettings` defaults to looking for env variables in uppercase. This issue is a feature proposal to make this behaviour configurable.

The current workaround is to use the settings config class to set up aliases, e.g.:

```python
 class S(pydantic.BaseSettings):
      test:str

      class Config:
          fields = dict(test=dict(alias="test"))
```
However this is verbose and onerous to do (for the developer), if there are many such env vars.

It would be nice if the developer could do something like:

```python
 class S(pydantic.BaseSettings):
      class Config:
          letter_case = "lower" # or:  "upper" | "any"

      test:str
```

About this concern raised for case insensitive option https://github.com/samuelcolvin/pydantic/issues/275#issuecomment-430336275.

* we could log a warning or raise an error at a certain configurable threshold of
    * env var count
    * or perhaps based on measured processing time exceeding configurable budget (e.g. 1ms)
* we could cache the calculation of `env2` (but probably the entire settings result would just be cached by the app)
* the env prefix could help reduce the amount of work needed to do
Allow configuring env letter case rules for settings parser
Issue derived from https://github.com/samuelcolvin/pydantic/issues/275

Currently `BaseSettings` defaults to looking for env variables in uppercase. This issue is a feature proposal to make this behaviour configurable.

The current workaround is to use the settings config class to set up aliases, e.g.:

```python
 class S(pydantic.BaseSettings):
      test:str

      class Config:
          fields = dict(test=dict(alias="test"))
```
However this is verbose and onerous to do (for the developer), if there are many such env vars.

It would be nice if the developer could do something like:

```python
 class S(pydantic.BaseSettings):
      class Config:
          letter_case = "lower" # or:  "upper" | "any"

      test:str
```

About this concern raised for case insensitive option https://github.com/samuelcolvin/pydantic/issues/275#issuecomment-430336275.

* we could log a warning or raise an error at a certain configurable threshold of
    * env var count
    * or perhaps based on measured processing time exceeding configurable budget (e.g. 1ms)
* we could cache the calculation of `env2` (but probably the entire settings result would just be cached by the app)
* the env prefix could help reduce the amount of work needed to do


Discussion:
I think just a warning in the docs is enough for the size thing.
I think just a warning in the docs is enough for the size thing.

Resolving patch:
diff --git a/docs/examples/settings_case_insensitive.py b/docs/examples/settings_case_insensitive.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/settings_case_insensitive.py
@@ -0,0 +1,8 @@
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+    redis_host = 'localhost'
+
+    class Config:
+        case_insensitive = True
diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py
--- a/pydantic/env_settings.py
+++ b/pydantic/env_settings.py
@@ -21,7 +21,9 @@ class BaseSettings(BaseModel):
     """
     Base class for settings, allowing values to be overridden by environment variables.
 
-    Environment variables must be upper case. Eg. to override foobar, `export APP_FOOBAR="whatever"`.
+    Environment variables must be upper case and prefixed by APP_ by default. Eg. to override foobar,
+    `export APP_FOOBAR="whatever"`. To change this behaviour set Config options case_insensitive and
+    env_prefix.
 
     This is useful in production for secrets you do not wish to save in code, it places nicely with docker(-compose),
     Heroku and any 12 factor app design.
@@ -36,23 +38,34 @@ def _substitute_environ(self):
         Substitute environment variables into values.
         """
         d = {}
+
+        if self.__config__.case_insensitive:
+            env_vars = {k.lower(): v for (k, v) in os.environ.items()}
+        else:
+            env_vars = os.environ
+
         for field in self.__fields__.values():
+
             if field.has_alias:
                 env_name = field.alias
             else:
                 env_name = self.__config__.env_prefix + field.name.upper()
-            env_var = os.getenv(env_name, None)
-            if env_var:
+
+            env_name_ = env_name.lower() if self.__config__.case_insensitive else env_name
+            env_val = env_vars.get(env_name_, None)
+
+            if env_val:
                 if _complex_field(field):
                     try:
-                        env_var = json.loads(env_var)
+                        env_val = json.loads(env_val)
                     except ValueError as e:
                         raise SettingsError(f'error parsing JSON for "{env_name}"') from e
-                d[field.alias] = env_var
+                d[field.alias] = env_val
         return d
 
     class Config:
-        env_prefix = 'APP_'
+        env_prefix = "APP_"
         validate_all = True
         ignore_extra = False
         arbitrary_types_allowed = True
+        case_insensitive = False


Test patch:
diff --git a/tests/test_settings.py b/tests/test_settings.py
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -110,3 +110,18 @@ class Config:
     env.set('foobar', 'xxx')
     s = Settings()
     assert s.foobar == 'xxx'
+
+
+def test_case_insensitive(env):
+    class Settings(BaseSettings):
+        foo: str
+        bAR: str
+
+        class Config:
+            case_insensitive = True
+
+    env.set('apP_foO', 'foo')
+    env.set('app_bar', 'bar')
+    s = Settings()
+    assert s.foo == 'foo'
+    assert s.bAR == 'bar'