Repository: pydantic/pydantic-settings

Issue:
CLI use of `IntEnum` not working with nested models
## Description

The [pydantic docs about the usage of enums in CLIs](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#literals-and-enums) state that `enum.IntEnum` can perfectly be used with CLIs, where the enum's field names are eventually accepted as flag values.

_[excerpt] from the docs:_

> CLI argument parsing of literals and enums are converted into CLI choices.

```python
import sys
from enum import IntEnum
from pydantic_settings import BaseSettings

class Fruit(IntEnum):
    pear = 0
    kiwi = 1
    lime = 2

class Settings(BaseSettings, cli_parse_args=True):
    fruit: Fruit

sys.argv = ['example.py', '--fruit', 'lime']
print(Settings().model_dump())
#> {'fruit': <Fruit.lime: 2>}
```

And this works totally fine. But as soon as a model is nested, it raises a validation error. For example, consider this scenario:

```python
from enum import IntEnum

class LogLevel(IntEnum):
    CRITICAL = 50
    ERROR = 40
    WARNING = 30
    INFO = 20
    DEBUG = 10

@pydantic.dataclasses.dataclass
class LoggingOptions:
    level: LogLevel = LogLevel.INFO

class Settings(BaseSettings):

    model_config = SettingsConfigDict(
        cli_parse_args=True,
        cli_kebab_case=True,
        cli_use_class_docs_for_groups=True,
        cli_avoid_json=True,
        frozen=True,
    )

    level: LogLevel = LogLevel.INFO # this works

    logging: LoggingOptions # this DOES NOT work
```

When tested, the root level flags work as intended with integer enums:

```python
 sys.argv = ['example.py', '--level', 'DEBUG']
 settings = Settings()
```

However, nested ones do not:

```python
 sys.argv = ['example.py', '--logging.level', 'DEBUG']
 settings = Settings()
```

and this will raise:

```bash
Traceback (most recent call last):
  File "/project/sandbox/main.py", line 55, in <module>
    settings = Settings()
               ^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/pydantic_settings/main.py", line 176, in __init__
    super().__init__(
  File "/usr/local/lib/python3.12/site-packages/pydantic/main.py", line 253, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
logging.level
  Input should be 50, 50, 40, 30, 30, 20, 10 or 0 [type=enum, input_value='DEBUG', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/enum
```

## Expected Behaviour

I expected this to work properly with nested models too, just like root level objects. However, I am not familiar enough with `pydantic` or `pydantic-settings` to tell whether this is intended / expected behaviour.

I searched the docs and issues, but couldn't find related information. Sorry, if this was already brought up.

## How to replicate

Note that the code of the above examples is simplified, but I created a codesandbox to replicate this: https://codesandbox.io/p/devbox/59rv9y?embed=1&file=%2Fmain.py

## Affected Versions

I tested with the latest versions of `pydantic` and `pydantic-settings`:

- `pydantic == 2.11.3`
- `pydantic-settings == 2.8.1`

and used Python 3.12 as well as 3.13.

Hints from issue comments:
@hramezani: @kschwab please take a look when you have time. sorry for pinging you too much, you are the only person who is expert in CLI.@kschwab: No worries @hramezani, happy to help. Opened #589 for resolution.@hramezani: Thanks @kschwab for the fix PR and sorry for asking you to fix a non cli bug. @lsg551: Greatly appreciate that, thanks!

Solution patch:
diff --git a/pydantic_settings/sources/providers/env.py b/pydantic_settings/sources/providers/env.py
index 5ab0537..5a350f1 100644
--- a/pydantic_settings/sources/providers/env.py
+++ b/pydantic_settings/sources/providers/env.py
@@ -242,6 +242,9 @@ class EnvSettingsSource(PydanticBaseEnvSettingsSource):
             if (target_field or is_dict) and env_val:
                 if target_field:
                     is_complex, allow_json_failure = self._field_is_complex(target_field)
+                    if self.env_parse_enums:
+                        enum_val = _annotation_enum_name_to_val(target_field.annotation, env_val)
+                        env_val = env_val if enum_val is None else enum_val
                 else:
                     # nested field type is dict
                     is_complex, allow_json_failure = True, True


Test patch:
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 5ca14d8..1800d54 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -2348,13 +2348,18 @@ def test_env_json_field(env):
 
 
 def test_env_parse_enums(env):
-    class Settings(BaseSettings):
+    class NestedEnum(BaseModel):
+        fruit: FruitsEnum
+
+    class Settings(BaseSettings, env_nested_delimiter='__'):
         fruit: FruitsEnum
         union_fruit: Optional[Union[int, FruitsEnum]] = None
+        nested: NestedEnum
 
     with pytest.raises(ValidationError) as exc_info:
         env.set('FRUIT', 'kiwi')
         env.set('UNION_FRUIT', 'kiwi')
+        env.set('NESTED__FRUIT', 'kiwi')
         s = Settings()
     assert exc_info.value.errors(include_url=False) == [
         {
@@ -2385,22 +2390,43 @@ def test_env_parse_enums(env):
             'msg': 'Input should be 0, 1 or 2',
             'type': 'enum',
         },
+        {
+            'ctx': {
+                'expected': '0, 1 or 2',
+            },
+            'input': 'kiwi',
+            'loc': (
+                'nested',
+                'fruit',
+            ),
+            'msg': 'Input should be 0, 1 or 2',
+            'type': 'enum',
+        },
     ]
 
     env.set('FRUIT', str(FruitsEnum.lime.value))
     env.set('UNION_FRUIT', str(FruitsEnum.lime.value))
+    env.set('NESTED__FRUIT', str(FruitsEnum.lime.value))
     s = Settings()
     assert s.fruit == FruitsEnum.lime
+    assert s.union_fruit == FruitsEnum.lime
+    assert s.nested.fruit == FruitsEnum.lime
 
     env.set('FRUIT', 'kiwi')
     env.set('UNION_FRUIT', 'kiwi')
+    env.set('NESTED__FRUIT', 'kiwi')
     s = Settings(_env_parse_enums=True)
     assert s.fruit == FruitsEnum.kiwi
+    assert s.union_fruit == FruitsEnum.kiwi
+    assert s.nested.fruit == FruitsEnum.kiwi
 
     env.set('FRUIT', str(FruitsEnum.lime.value))
     env.set('UNION_FRUIT', str(FruitsEnum.lime.value))
+    env.set('NESTED__FRUIT', str(FruitsEnum.lime.value))
     s = Settings(_env_parse_enums=True)
     assert s.fruit == FruitsEnum.lime
+    assert s.union_fruit == FruitsEnum.lime
+    assert s.nested.fruit == FruitsEnum.lime
 
 
 def test_env_parse_none_str(env):