Repository: dynaconf/dynaconf
Pull request #1110 (2024-06-21 20:58:52)

Adding more tests and scenarios to typing
## Tests

- [x] Use dynaconf_options to set Dynaconf initialization
- [x] Set validation as immediate or lazy
- [x] Validate type based on annotated type `name: T`
- [x] handle nested subtype
- [x] Set default based `name: T = default`
- [x] Extract Validators from Annotated `field: Annotated[T, Validator()]`
- [x] Forbid validators with defaults,is_type_of,names,must_exist,required
- [x] Forbid Annotated with types out of the allowed list
- [x] Forbid args to Annotated that are not Validator `Annotated[T, strange_thing]`
- [x] Handle Union types `field: Union[T, T, T]`
- [x] Handle Optional types `field: Optional[T]` and `field: Union[T, None]`
- [x] Handle list enclosed types `field: list[T]`  (and with default)
- [x] Handle empty list, must be allowed `field: list[T]` accepts `[]`
- [x] Handle list enclosed union `field: list[Union[T, T, T]]`
- [x] Handle list enclosed optional `field: list[Optional[T]]`
- [x] Handle list enclosed new union `field: list[T | T]`
- [x] Forbid list enclosed with more than one arg `list[T, T]`
- [x] Handle Annotated + enclosed type `field: Annotated[list[T], Validator()]`
- [x] Handle Annotated + union type `field: Annotated[Union[T, T], Validator()]`
- [x] Handle Annotated + optional `field: Annotated[Optional[T], Validator()]`
- [x] Handle Annotated + new union `field: Annotated[T | T, Validator()]`
- [x] Forbid Nested Subtype on Annotated `Annotated[Nested, ...]`
- [x] Handle SubType/Map/Nested `class Struct: ...` - `field: Struct`
- [x] Handle Optional DictValue `field: Optional[Foo]` and `NotRequired[Foo]`
- [x] Handle deeply nested Subtype (multiple levels)
- [x] Handle enclosed SubType `field: list[Struct]`
- [x] Handle deeply nested Subtype (multiple levels) with enclosed types
- [x] Forbid enclosed subtype with defaults
- [x] Forbid enclosed subtypes that are not on allowed list `field: list[A]`
- [x] Forbid union subtypes that are not on allowed list `field: list[Union[A]]`
- [x] Handle dict type `field: dict[T, T]` + Annotated and enclosed

## Typing changes

- [x] Implement NotRequired, for `Annotated[int, NotRequired]` (omitable key)
- [x] Rename Nested to DictValue 

## Ideas

- [x] Move typed.py to typed/main.py
- [x] Move helpers to typed/utils.py
- [x] Define ALLOWED_TYPES as type aliases on typed/types.py
- [x] Change Validator to accept a named list of operations


Unresolved items now tracked on https://github.com/dynaconf/dynaconf/issues/1082#issuecomment-2199703596

github-actions[bot]: ## Test Results (Python 3.9)
506 tests  ±0   506 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") ±0   38s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") +2s
    1 suites ±0       0 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0 
    1 files   ±0       0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 

Results for commit 2769d393. ± Comparison against base commit 1e39d6e9.

[test-results]:data:application/gzip;base64,H4sIAG7qdWYC/13MOQ7CMBRF0a1Erik8xjabQZ4ifZHEyEOF2DtOgAyU5z7pPdEAY8jo2pFLh3KFssHXZArEuZGp5raUZRO4/+mWq3P/6Q6PlvAWBgPjKYSUYvqWVOf9c8H58lP2x9WHw9XHPxenCUoDorLXnmmmORGUeSNNcFJJzSW1VmGLuZDCEodeb93c21sEAQAA

github-actions[bot]: ## Test Results (Python 3.12)
506 tests  ±0   506 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") ±0   50s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") +2s
    1 suites ±0       0 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0 
    1 files   ±0       0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 

Results for commit 2769d393. ± Comparison against base commit 1e39d6e9.

[test-results]:data:application/gzip;base64,H4sIAIfqdWYC/13MRw7CMBCF4atEXrNwzcRcJnKLZJHEyGWFuDvGQArL743mf6DJzy6ha0cuHUrF5w22RJV9WCsFrq6XnBr6n8ZUjPmfbv5ep+1jnJSfT4OLMcTvEsu6N984Jz/LXmw+BJuPPROWxecKRKGXlkkmORGUWQXKGRhAcqBaD1hjLkBoYtDzBf2tPL0EAQAA


Patch:
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 279f3a0..3edc9a1 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -226,6 +226,13 @@ class Settings:
         :param settings_module: defines the settings file
         :param kwargs:  override default settings
         """
+        if "dynaconf_options" in kwargs:
+            warnings.warn(
+                "dynaconf_options: Options works only with typed Dynaconf"
+                "Change your import to: `from dynaconf.typed import Dynaconf`",
+                RuntimeWarning,
+            )
+
         self._fresh = False
         self._loaded_envs = []
         self._loaded_hooks = defaultdict(dict)
diff --git a/dynaconf/typed.py b/dynaconf/typed.py
deleted file mode 100644
index 9f270f7..0000000
--- a/dynaconf/typed.py
+++ /dev/null
@@ -1,299 +0,0 @@
-"""Implements wrappers, types and helpers for Schema based settings."""
-
-from __future__ import annotations  # WARNING: remove this when debugging
-
-from dataclasses import asdict
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Annotated
-from typing import Callable
-from typing import cast
-from typing import get_args
-from typing import get_origin
-from typing import Union
-
-from dynaconf.base import LazySettings as OriginalDynaconf
-from dynaconf.base import Settings as BaseDynaconfSettings
-from dynaconf.validator import ValidationError  # noqa
-from dynaconf.validator import Validator
-
-
-class Empty: ...
-
-
-empty = Empty()
-
-
-@dataclass  # wanted to pass (kw_only=True) but that works only for 3.10+
-class Options:
-    """Options to configure the initialization of Dynaconf Object.
-
-    NOTE: This must be used only with typed implementation of Dynaconf.
-
-    All options are just bypassed to the Dynaconf `__init__` method,
-    except those starting with `_`.
-
-    Options starting with `_` are only used on `__new__` when building the
-    new Dynaconf object, those values assumes opinionated defaults and rarely
-    needs to be changed.
-    """
-
-    # NOTE: On a dataclass the sentinel must be a Type not an instance
-
-    env: str | type = Empty
-    environments: bool | type = Empty
-    envvar_prefix: str | type = Empty
-    root_path: str | Path | type = Empty
-    settings_files: list[str] | type = Empty
-    settings_file: str | type = Empty
-
-    _trigger_validation: bool = True
-
-    def as_dict(self):
-        options_dict = asdict(self)
-        options_dict = {
-            k: v
-            for k, v in options_dict.items()
-            if (not k.startswith("_")) and v is not Empty
-        }
-        return options_dict
-
-
-class DynaconfSchemaError(Exception): ...
-
-
-class Nested: ...
-
-
-class Dynaconf(BaseDynaconfSettings):
-    """Implementation of Dynaconf that is aware of type annotations."""
-
-    def __new__(cls, *args, **kwargs):
-        options = getattr(cls, "dynaconf_options", Options())
-        defaults, validators = extract_defaults_and_validators_from_typing(cls)
-        passed_in_validators = kwargs.pop("validators", [])
-        validators += passed_in_validators
-
-        # Set init options for Dynaconf coming first from Options on Schema
-        init_options = options.as_dict()
-        # Add defaults defined on class as it was passed to Dynaconf(k=v)
-        init_options.update(defaults)
-        # Override from kwargs as if matching key was passed to Settings(k=v)
-        init_options.update(kwargs)
-
-        # Instantiate the new Dynaconf instance
-        new_cls = OriginalDynaconf(*args, **init_options)
-        new_cls.__annotations__ = cls.__annotations__
-
-        if validators:
-            new_cls.validators.register(*validators)
-
-        # Trigger validation if not explicitly disabled
-        if options._trigger_validation:
-            new_cls.validators.validate_all()
-
-        return cast(cls, new_cls)
-
-
-def is_union(annotation) -> bool:
-    """Tell if an annotation is strictly Union."""
-    return get_origin(annotation) is Union
-
-
-def is_optional(annotation) -> bool:
-    """Tell if an annotation is strictly typing.Optional or Union[**, None]"""
-    return is_union(annotation) and type(None) in get_args(annotation)
-
-
-# Types that can be used on `field: list[x]`
-ALLOWED_ENCLOSED_TYPES = (
-    Nested,
-    int,
-    float,
-    str,
-    bool,
-    type(None),
-    list,
-    tuple,
-    dict,
-)
-
-
-def get_annotations(schema_cls) -> dict:
-    """NOTE: move to inspect.get_annotations when Python 3.9 is dropped"""
-    if isinstance(schema_cls, type):
-        return schema_cls.__dict__.get("__annotations__", {})
-    else:
-        return getattr(schema_cls, "__annotations__", {})
-
-
-def get_type_name(_type):
-    if name := getattr(_type, "__name__", None):
-        return name
-    return str(_type)
-
-
-def extract_defaults_and_validators_from_typing(
-    schema_cls,
-    path: tuple | None = None,
-    defaults: dict | None = None,
-    validators: list | None = None,
-) -> tuple[dict, list]:
-    """From schema class extract defaults and validators from annotations.
-
-    Recursively handle all `Nested` subtypes.
-
-    key: int -> Validator(key, is_type_of=int)
-    key: int = 1 -> Validator(key, is_type_of=int) + defaults[key] = 1
-    key: Annotated[int, Validator()] -> Validator(key, is_type_of=int)
-    """
-    path = path or tuple()
-    defaults = defaults or {}
-    validators = validators or []
-    schema_annotations = get_annotations(schema_cls)
-
-    for name, annotation in schema_annotations.items():
-        default_value = getattr(schema_cls, name, empty)
-        full_path = path + (name,)  # E.g, ("path", "to", "name")
-        full_name = ".".join(full_path)  # E.g, path.to.name
-        _annotated_validators = None
-        _type_args: tuple = tuple()
-
-        _type = get_origin(annotation)  # type from X[type,]
-        if _type is Annotated:
-            _type, *_annotated_validators = get_args(annotation)
-        else:
-            _type_args = get_args(annotation)
-
-        if default_value is not empty:
-            defaults[full_name] = default_value
-
-        # it is a field: Nested
-        if isinstance(annotation, type) and issubclass(annotation, Nested):
-            defaults, validators = extract_defaults_and_validators_from_typing(
-                annotation, full_path, defaults, validators
-            )
-        # It is a field: Annotated[type, Validator(...)]
-        elif _annotated_validators:
-            if isinstance(_type, type) and issubclass(_type, Nested):
-                raise DynaconfSchemaError(
-                    "Nested type cannot be Annotated, "
-                    f"set the validators on the {_type.__name__!r} fields "
-                    f"this error is caused by "
-                    f"`{full_name}: Annotated[{_type.__name__}, *]`"
-                )
-
-            for _validator in _annotated_validators:
-                _validator.names = [full_name]
-                _validator.is_type_of = _type
-                if default_value is empty and not is_optional(annotation):
-                    _validator.must_exist = True  # required
-                validators.append(_validator)
-
-        # it is a field: list[Nested] (or any ALLOWED_ENCLOSED_TYPES)
-        elif _type_args and _type in (list, tuple):
-            _validator = Validator(full_name, is_type_of=_type)
-            # ADR: Decided to consider only the first enclosed type
-            # which means T[T] is allowed, but T[T, T] not
-            # This is subject to change in future, PRs welcome.
-            inner_type, *more_enclosed_types = _type_args
-            if more_enclosed_types:
-                raise DynaconfSchemaError(
-                    f"Invalid enclosed type for {full_name} "
-                    "enclosed types supports only one argument "
-                    f"{get_type_name(_type)}[{get_type_name(inner_type)}] "
-                    "is allowed, but this error is caused by "
-                    f"`{full_name}: {get_type_name(_type)}[{get_type_name(inner_type)}, "
-                    f"{', '.join(get_type_name(i) for i in more_enclosed_types)}]`"
-                )
-
-            # # Handle list[Union...]
-            if is_union(inner_type):
-                all_types = get_args(inner_type)
-            else:
-                all_types = (inner_type,)
-
-            for item in all_types:
-                if not issubclass(item, ALLOWED_ENCLOSED_TYPES):
-                    raise DynaconfSchemaError(
-                        f"Invalid enclosed type {get_type_name(item)!r} "
-                        f"must be one of {ALLOWED_ENCLOSED_TYPES} "
-                        f"this error is caused by "
-                        f"`{full_name}: {get_type_name(_type)}[{get_type_name(inner_type)}]`"
-                    )
-
-            enclosed_defaults, enclosed_validators = (
-                extract_defaults_and_validators_from_typing(inner_type)
-            )
-            if enclosed_defaults:
-                raise DynaconfSchemaError(
-                    "List enclosed types cannot define default values. "
-                    f"{inner_type.__name__!r} defines {enclosed_defaults}. "
-                    f"this error is caused by "
-                    f"`{full_name}: {_type.__name__}[{inner_type.__name__}]`"
-                )
-
-            _validator.condition = validator_condition_factory(
-                enclosed_validators, full_name, inner_type
-            )
-
-            if default_value is empty and not is_optional(annotation):
-                _validator.must_exist = True  # required
-
-            validators.append(_validator)
-
-        # It is just a bare type annotation like field: type
-        else:
-            _validator = Validator(full_name, is_type_of=annotation)
-            if default_value is empty and not is_optional(annotation):
-                _validator.must_exist = True  # required
-            validators.append(_validator)
-
-    return defaults, validators
-
-
-def validator_condition_factory(validators, prefix, _type) -> Callable:
-    """takes _type: T and generates a function to validate a value against it"""
-
-    def validator_condition(values):
-        if isinstance(_type, type) and issubclass(_type, Nested):  # a dict
-            validator_messages = {
-                "must_exist_true": prefix
-                + "[].{name} is required in env {env}",
-                "must_exist_false": prefix
-                + "[].{name} cannot exists in env {env}",
-                "condition": prefix
-                + "[].{name} invalid for {function}({value}) in env {env}",
-                "operations": (
-                    prefix + "[].{name} must {operation} {op_value} "
-                    "but it is {value} in env {env}"
-                ),
-            }
-            for value in values:
-                for validator in validators:
-                    validator.messages.update(validator_messages)
-                    validator.validate(BaseDynaconfSettings(**value))
-        else:  # not a dict
-            validator_messages = {
-                "must_exist_true": "{name}[] is required in env {env}",
-                "must_exist_false": "{name}[] cannot exists in env {env}",
-                "condition": "{name}[] invalid for {function}({value}) in env {env}",
-                "operations": (
-                    "{name}[] must {operation} {op_value} "
-                    "but it is {value} in env {env}"
-                ),
-            }
-            for value in values:
-                _settings = BaseDynaconfSettings()
-                _settings.set(prefix, value)
-                _validator = Validator(
-                    prefix,
-                    is_type_of=_type,
-                    messages=validator_messages,
-                )
-                _validator.validate(_settings)
-
-        # No validation error
-        return True
-
-    return validator_condition
diff --git a/dynaconf/typed/__init__.py b/dynaconf/typed/__init__.py
new file mode 100644
index 0000000..a4b573b
--- /dev/null
+++ b/dynaconf/typed/__init__.py
@@ -0,0 +1,21 @@
+from .exceptions import DynaconfSchemaError
+from .exceptions import ValidationError
+from .main import Dynaconf
+from .main import Options
+from .types import Annotated
+from .types import DictValue
+from .types import ItemsValidator
+from .types import NotRequired
+from .types import Validator
+
+__all__ = [
+    "Annotated",
+    "DictValue",
+    "Dynaconf",
+    "DynaconfSchemaError",
+    "ItemsValidator",
+    "NotRequired",
+    "Options",
+    "Validator",
+    "ValidationError",
+]
diff --git a/dynaconf/typed/compat.py b/dynaconf/typed/compat.py
new file mode 100644
index 0000000..bae4896
--- /dev/null
+++ b/dynaconf/typed/compat.py
@@ -0,0 +1,24 @@
+import sys
+
+
+def _get_annotations_for_python_39(schema_cls) -> dict:
+    if isinstance(schema_cls, type):
+        return schema_cls.__dict__.get("__annotations__", {})
+    else:
+        return getattr(schema_cls, "__annotations__", {})
+
+
+if sys.version_info < (3, 10):
+    from typing import Union
+
+    UnionType = Union
+    get_annotations = _get_annotations_for_python_39
+else:
+    from inspect import get_annotations
+    from types import UnionType
+
+
+__all__ = [
+    "get_annotations",
+    "UnionType",
+]
diff --git a/dynaconf/typed/exceptions.py b/dynaconf/typed/exceptions.py
new file mode 100644
index 0000000..8081b9f
--- /dev/null
+++ b/dynaconf/typed/exceptions.py
@@ -0,0 +1,7 @@
+from dynaconf.validator import ValidationError
+
+
+class DynaconfSchemaError(Exception): ...
+
+
+__all__ = ["DynaconfSchemaError", "ValidationError"]
diff --git a/dynaconf/typed/guards.py b/dynaconf/typed/guards.py
new file mode 100644
index 0000000..94807be
--- /dev/null
+++ b/dynaconf/typed/guards.py
@@ -0,0 +1,117 @@
+from dynaconf.utils.functional import empty
+from dynaconf.validator import Validator as BaseValidator
+from dynaconf.validator_conditions import is_type_of
+
+from . import exceptions as ex
+from . import types as ty
+from . import utils as ut
+
+
+def raise_for_invalid_annotated_args(full_name, _type, _type_args) -> None:
+    for arg in _type_args:
+        if isinstance(arg, (BaseValidator, ty.NotRequiredMarker)):
+            continue
+
+        raise ex.DynaconfSchemaError(
+            f"Invalid Annotated Args "
+            f"all items must be instances of Validator "
+            f"this error is caused by "
+            f"`{full_name}: Annotated[{ut.get_type_name(_type)!r}"
+            f"[{', '.join(str(i) for i in _type_args)}]`"
+        )
+
+
+def raise_for_invalid_amount_of_enclosed_types(
+    full_name, _type, inner_type, more_types
+) -> None:
+    """Decided to consider only the first enclosed type
+    which means list[T] is allowed, but list[T, T] not
+    This is subject to change in future.
+    """
+    if more_types:
+        raise ex.DynaconfSchemaError(
+            f"Invalid enclosed type for {full_name} "
+            "enclosed types supports only one argument "
+            f"{ut.get_type_name(_type)}[{ut.get_type_name(inner_type)}] "
+            "is allowed, but this error is caused by "
+            f"`{full_name}: {ut.get_type_name(_type)}"
+            f"[{ut.get_type_name(inner_type)}, "
+            f"{', '.join(ut.get_type_name(i) for i in more_types)}]`"
+        )
+
+
+def raise_for_unsupported_type(full_name, _type, _type_args, annotation):
+    # Remove any Validator()|NotRequiredMarker from extracted _type_args
+    _type_args = [
+        item
+        for item in _type_args
+        if not isinstance(item, (BaseValidator, ty.NotRequiredMarker))
+    ]
+
+    _types = _type_args or [_type]
+
+    for _type in _types:
+        _type_origin = ut.get_origin(_type) or _type
+        _type_args = ut.get_args(_type)
+        if _type_args:  # Union[] or list[] or tuple[]
+            raise_for_unsupported_type(
+                full_name, _type, _type_args, annotation
+            )
+
+        unsupported = isinstance(_type, type) and not issubclass(
+            _type_origin, ty.SUPPORTED_TYPES
+        )
+        if unsupported:
+            raise ex.DynaconfSchemaError(
+                f"Invalid type {ut.get_type_name(_type)!r} "
+                f"must be one of {ty.SUPPORTED_TYPES} "
+                f"this error is caused by "
+                f"`{full_name}: {annotation}`"
+            )
+
+
+def raise_for_optional_without_none(
+    full_name, _type, _type_args, default_value, annotation
+):
+    # NOTE: Not sure if we allow Optional[T] to also have T as default..
+    if default_value is not None and ut.is_optional(annotation):
+        raise ex.DynaconfSchemaError(
+            f"Optional Union "
+            "must assign `None` explicitly as default. "
+            f"try changing to: `{full_name}: {annotation} = None` "
+            f"this error is caused by "
+            f"`{full_name}: {annotation}`. "
+            "Did you mean to declare as `NotRequired` "
+            f"instead of {ut.get_type_name(annotation)!r}?"
+        )
+
+
+def raise_for_invalid_class_variable(cls):
+    """typed.Dynaconf only allows typed variables or dynaconf_options attr"""
+    for name, value in vars(cls).items():
+        if (
+            name.startswith(("_", "dynaconf_options"))
+            or name in cls.__annotations__
+        ):
+            continue
+        msg = (
+            f"Invalid assignment on `{ut.get_type_name(cls)}:"
+            f" {name} = {ut.get_type_name(value)}`. "
+            "All attributes must include type annotations or "
+            "be a `dynaconf_options = Options(...)`. "
+        )
+        if ut.is_type(value):
+            msg += f"Did you mean '{name}: {ut.get_type_name(value)}'?"
+        else:
+            msg += f"Did you mean '{name}: T = {value}'?"
+
+        raise ex.DynaconfSchemaError(msg)
+
+
+def raise_for_invalid_default_type(full_name, value, other):
+    if value is not empty and not is_type_of(value, other):
+        raise ex.DynaconfSchemaError(
+            f"Invalid default value for {full_name}. "
+            f"The expected type is {other} "
+            f" but current type is {type(value)}"
+        )
diff --git a/dynaconf/typed/main.py b/dynaconf/typed/main.py
new file mode 100644
index 0000000..f5309f3
--- /dev/null
+++ b/dynaconf/typed/main.py
@@ -0,0 +1,235 @@
+from __future__ import annotations
+
+# WARNING: remove the import above when debugging on Python 3.12
+# otherwise type annotations will be stringified.
+import sys
+from dataclasses import asdict
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+from typing import cast
+
+from dynaconf.base import LazySettings
+from dynaconf.base import Settings as BaseDynaconfSettings
+from dynaconf.utils.functional import Empty
+from dynaconf.utils.functional import empty
+from dynaconf.validator import Validator as BaseValidator
+
+from . import guards as gu
+from . import types as ty
+from . import utils as ut
+from .compat import get_annotations
+
+if sys.version_info < (3, 10):
+    dclass_args: dict[str, Any] = {}
+else:
+    dclass_args = {"kw_only": True}
+
+
+@dataclass(**dclass_args)
+class Options:
+    """Options to configure the initialization of typed.Dynaconf Object.
+
+    All options are just bypassed to the Dynaconf `__init__` method,
+    except those starting with `_`.
+
+    Options starting with `_` are only used on `__new__` when building the
+    new Dynaconf object, those values assumes opinionated defaults and rarely
+    needs to be changed.
+    """
+
+    # NOTE: On a dataclass the sentinel must be a Type not an instance
+
+    env: str | type = Empty
+    environments: bool | type = Empty
+    envvar_prefix: str | type = Empty
+    root_path: str | Path | type = Empty
+    settings_files: list[str] | type = Empty
+    settings_file: str | type = Empty
+
+    _trigger_validation: bool = True
+
+    def as_dict(self):
+        options_dict = asdict(self)
+        options_dict = {
+            k: v
+            for k, v in options_dict.items()
+            if (not k.startswith("_")) and v is not Empty
+        }
+        return options_dict
+
+
+class Dynaconf(BaseDynaconfSettings):
+    """Interface to create an instance of Dynaconf based on typing.
+
+    This class is a builder for a new instance of Dynaconf with validators
+    and default values extracted from subclass type annotations and attributes.
+    """
+
+    dynaconf_options: Options
+
+    def __new__(cls, *args, **kwargs):
+        gu.raise_for_invalid_class_variable(cls)
+        options = getattr(cls, "dynaconf_options", Options())
+        if not isinstance(options, Options):
+            raise TypeError(
+                "dynaconf_options must be an instance of `Options`"
+            )
+        defaults, validators = extract_defaults_and_validators_from_typing(cls)
+        passed_in_validators = kwargs.pop("validators", [])
+        validators += passed_in_validators
+
+        init_options = options.as_dict()
+        merged_defaults = ut.dict_merge(defaults, kwargs)
+        init_options.update(merged_defaults)
+
+        # Useful while writing tests, to be removed later.
+        if kwargs.pop("_debug_mode", None):
+            ut.dump_debug_info(init_options, validators)
+
+        new_cls = LazySettings(*args, **init_options)
+        new_cls.__annotations__ = cls.__annotations__
+
+        if validators:
+            new_cls.validators.register(*validators)
+
+        # Trigger validation if not explicitly disabled
+        if options._trigger_validation:
+            new_cls.validators.validate_all()
+
+        return cast(cls, new_cls)
+
+
+def extract_defaults_and_validators_from_typing(
+    schema_cls,
+    path: tuple | None = None,
+) -> tuple[dict, list]:
+    """Extract defaults and validators from class type annotations."""
+    path = path or tuple()
+    defaults = {}
+    validators = []
+    for name, annotation in get_annotations(schema_cls).items():
+        if name == "dynaconf_options":
+            # In case someone does dynaconf_options: Options = Options(...)
+            # That value is kept out of the schema validation
+            continue
+
+        # paths for error reporting only
+        full_path = path + (name,)  # E.g, ("path", "to", "name")
+        full_name = ".".join(full_path)  # E.g, path.to.name
+
+        default_value = getattr(schema_cls, name, empty)
+
+        _type, _type_args = ut.get_type_and_args(annotation)
+        gu.raise_for_unsupported_type(full_name, _type, _type_args, annotation)
+        gu.raise_for_optional_without_none(
+            full_name, _type, _type_args, default_value, annotation
+        )
+        extra_validators = []
+        validator = BaseValidator(name, is_type_of=annotation)
+        required = default_value is empty and not ut.is_notrequired(annotation)
+        if required:
+            validator.must_exist = True
+
+        # field: DictValue
+        if ut.is_dict_value(annotation):
+            gu.raise_for_invalid_default_type(
+                full_name, default_value, annotation
+            )
+            items_defaults, items_validators = (
+                extract_defaults_and_validators_from_typing(
+                    annotation, full_path
+                )
+            )
+            validator.items_validators = items_validators
+            if items_defaults and not isinstance(default_value, Empty):
+                default_value = ut.dict_merge(items_defaults, default_value)
+
+        # field: Annotated[T, Validator(...)] or field: NotRequired[T]
+        elif ut.is_annotated(annotation):
+            gu.raise_for_invalid_default_type(full_name, default_value, _type)
+            gu.raise_for_invalid_annotated_args(full_name, _type, _type_args)
+            # set the annotated type for type checking
+            validator.is_type_of = _type
+            # Add all the extra validators added via Annotated args
+            marked_not_required = False
+            for _validator in _type_args:
+                # NotRequired adds a marker in the _type_args
+                if isinstance(_validator, ty.NotRequiredMarker):
+                    marked_not_required = True
+                    continue
+                _validator.names = (name,)
+                extra_validators.append(_validator)
+
+            if ut.is_dict_value(_type):
+                items_defaults, items_validators = (
+                    extract_defaults_and_validators_from_typing(
+                        _type, full_path
+                    )
+                )
+                validator.items_validators = items_validators
+                if not marked_not_required:
+                    if items_defaults and default_value is empty:
+                        default_value = {}
+                    if isinstance(default_value, dict) and isinstance(
+                        items_defaults, dict
+                    ):
+                        default_value = ut.dict_merge(
+                            items_defaults, default_value
+                        )
+
+        # field: list[T]
+        elif ut.is_enclosed_list(_type, _type_args):
+            # NOTE: Should we really avoid multi args? list[T, T, T] ?
+            inner_type, *more_types = _type_args
+            gu.raise_for_invalid_amount_of_enclosed_types(
+                full_name, _type, inner_type, more_types
+            )
+            gu.raise_for_invalid_default_type(
+                full_name, default_value, annotation
+            )
+            if ut.is_dict_value(inner_type):
+                items_defaults, items_validators = (
+                    extract_defaults_and_validators_from_typing(
+                        inner_type, full_path
+                    )
+                )
+                if isinstance(default_value, list):
+                    inner_defaults = []
+                    for item in default_value:
+                        inner_defaults.append(
+                            ut.dict_merge(items_defaults, item)
+                        )
+                    default_value = inner_defaults
+                validator.items_validators = items_validators
+
+        # field: Optional[T]
+        elif ut.is_optional(annotation):
+            if ut.is_dict_value(_type_args[0]):
+                gu.raise_for_invalid_default_type(
+                    full_name, default_value, annotation
+                )
+                _, items_validators = (
+                    extract_defaults_and_validators_from_typing(
+                        _type_args[0], full_path
+                    )
+                )
+                extra_validator = BaseValidator(
+                    name,
+                    items_validators=items_validators,
+                    when=BaseValidator(name, is_type_of=ty.DictValue),
+                )
+                extra_validators.append(extra_validator)
+
+        # field: T
+        else:
+            # NOTE: hook creation of validators for
+            # Optional and Notrequired here
+            ...
+
+        validators.append(validator)
+        validators.extend(extra_validators)
+        if default_value is not empty:
+            defaults[name] = default_value
+
+    return defaults, validators
diff --git a/dynaconf/typed/types.py b/dynaconf/typed/types.py
new file mode 100644
index 0000000..e5e6d6c
--- /dev/null
+++ b/dynaconf/typed/types.py
@@ -0,0 +1,170 @@
+from __future__ import annotations  # WARNING: remove this when debugging
+
+from collections.abc import Sequence
+from typing import Annotated
+from typing import Any
+from typing import Callable
+from typing import TypeVar
+from typing import Union
+
+from dynaconf.utils.functional import Empty
+from dynaconf.utils.functional import empty
+from dynaconf.validator import Validator as BaseValidator
+
+from .compat import get_annotations
+
+T = TypeVar("T")
+
+
+class NotRequiredMarker:
+    __slots__ = ()
+
+
+NotRequired = Annotated[T, NotRequiredMarker()]
+"""
+This type will mark keys as NotRequired which means it can be completely
+absent from the settings object.
+This differs from Optional, as Optional requires T or None,
+NotRequired requires T or simply nothing at all.
+In practice this will make the generated Validator to have required=False
+and skip the check for a default value, so settings.key will raise for
+AttributeError.
+
+When used as `NotRequired[T]`
+    _type = T, _type_args = (_NotRequiredMarker(),)
+When uses as `Annotated[NotRequired[T], Validator()]`
+    _type = T
+    _type_args (_NotRequiredMarker(), Validator())
+
+"""
+
+
+class DictValue:
+    """DictValue represents a Typed Dict in the settings.
+
+    When instantiating just returns a dict containing the default values only,
+    this type does not performs any validation.
+    """
+
+    __slots__ = ()
+    _dynaconf_dictvalue = True  # a marker for validator_conditions.is_type_of
+
+    def __new__(cls, **kwargs):
+        defaults = {}
+        schema_annotations = get_annotations(cls)
+        for name, annotation in schema_annotations.items():
+            value = getattr(cls, name, empty)
+            if value is not empty:
+                defaults[name] = value
+        defaults.update(kwargs)
+        return defaults
+
+
+class Validator:
+    """Define an interface to generate a valid BaseValidator.
+
+    The `operations` are::
+
+        eq: value == other
+        ne: value != other
+        gt: value > other
+        lt: value < other
+        gte: value >= other
+        lte: value <= other
+        is_type_of: isinstance(value, type)
+        is_in:  value in sequence
+        is_not_in: value not in sequence
+        identity: value is other
+        cont: contain value in
+        len_eq: len(value) == other
+        len_ne: len(value) != other
+        len_min: len(value) > other
+        len_max: len(value) < other
+        startswith: value.startswith(term)
+        endswith:  value.endswith(term)
+        condition: fn(value) is True
+
+
+    """
+
+    __slots__ = ()
+
+    def __new__(
+        cls,
+        *,  # kw_only
+        # Operations
+        eq: Any = empty,
+        ne: Any = empty,
+        gt: Any = empty,
+        lt: Any = empty,
+        gte: Any = empty,
+        lte: Any = empty,
+        identity: Any = empty,
+        is_in: Any = empty,
+        is_not_in: Any = empty,
+        cont: Any = empty,
+        len_eq: Any = empty,
+        len_ne: Any = empty,
+        len_min: Any = empty,
+        len_max: Any = empty,
+        startswith: Any = empty,
+        endswith: Any = empty,
+        condition: Callable[[Any], bool] | None | Empty = empty,
+        # options
+        env: str | Sequence[str] | None | Empty = empty,
+        messages: dict[str, str] | None | Empty = empty,
+        cast: Callable[[Any], Any] | None | Empty = empty,
+        description: str | None | Empty = empty,
+        items_validators: list[BaseValidator] | None = None,
+        # To be implemented, a interface like this to generate a When Validator
+        # when: When | None = empty,
+    ):
+        kwargs = locals()
+        kwargs.pop("cls")
+        return BaseValidator(
+            **{k: v for k, v in kwargs.items() if v is not empty}
+        )
+
+
+class ItemsValidator:
+    """Interface to instantiate a new Validator with items_validators"""
+
+    __slots__ = ()
+
+    def __new__(cls, *args):
+        return BaseValidator(items_validators=args)
+
+
+"""
+Aliases for supported types
+That can be used with Annotated, Union and T[T]
+These are defined here mainly to help with auto complete
+
+    from dynaconf.typed import types as ts
+    class Settings(Dynaconf):
+        field: ts.Str
+        # actually the same as
+        field: str
+
+"""
+Str = str
+Int = int
+Float = float
+Bool = bool
+NoneType = type(None)  # python 3.9 doesn't have types.NoneType
+List = list
+Tuple = tuple
+Dict = dict
+Numeric = Union[int, float]
+
+SUPPORTED_TYPES = (
+    Str,
+    Int,
+    Float,
+    Bool,
+    NoneType,
+    List,
+    Tuple,
+    Dict,
+    DictValue,
+)
diff --git a/dynaconf/typed/utils.py b/dynaconf/typed/utils.py
new file mode 100644
index 0000000..0aa1e98
--- /dev/null
+++ b/dynaconf/typed/utils.py
@@ -0,0 +1,162 @@
+from __future__ import annotations  # WARNING: remove this when debugging
+
+from typing import Annotated
+from typing import Callable
+from typing import get_args
+from typing import get_origin
+from typing import Union
+
+from dynaconf.base import Settings as BaseDynaconfSettings
+from dynaconf.validator import Validator as BaseValidator
+
+from . import types as ty
+from .compat import UnionType
+
+
+def get_all_enclosed_types(inner_type):
+    # # Handle list[Union...]
+    if is_union(inner_type):
+        all_types = get_args(inner_type)
+    else:
+        all_types = (inner_type,)
+    return all_types
+
+
+def get_type_name(_type):
+    if name := getattr(_type, "__name__", None):
+        return name
+    return str(_type)
+
+
+def get_type_and_args(annotation) -> tuple:
+    """From annotation name: T[T, args] extract type, args."""
+    _type_args: tuple = tuple()
+    # _type from `field: T` or `field: list[T]`
+    _type = get_origin(annotation) or annotation
+    if _type is Annotated:
+        _type, *_type_args = get_args(annotation)
+    else:
+        _type_args = get_args(annotation)
+    return _type, _type_args
+
+
+def is_notrequired(annotation):
+    return is_annotated(annotation) and any(
+        isinstance(item, ty.NotRequiredMarker) for item in get_args(annotation)
+    )
+
+
+def is_annotated(annotation):
+    return get_origin(annotation) is Annotated
+
+
+def is_dict_value(annotation):
+    return isinstance(annotation, type) and issubclass(
+        annotation, ty.DictValue
+    )
+
+
+def is_enclosed_list(_type, _type_args):
+    return _type_args and _type in (list, tuple)
+
+
+def is_union(annotation) -> bool:
+    """Tell if an annotation is strictly Union."""
+    return get_origin(annotation) in [Union, UnionType]
+
+
+def is_optional(annotation) -> bool:
+    """Tell if an annotation is strictly typing.Optional or Union[**, None]"""
+    return is_union(annotation) and get_args(annotation)[1] == type(None)
+
+
+def is_type(value) -> bool:
+    _type = get_origin(value) or value
+    return isinstance(_type, type) or is_union(value) or is_optional(value)
+
+
+def condition_factory_for_list_items(validators, prefix, _type) -> Callable:
+    """takes _type: T, generates a function to validate a value against it"""
+    # IDEA:
+    # Evaluate the possibility to replace this with a new attribute on a
+    # validator that will validate against each internal item of a list
+    # Validator("X", is_type_of=list, items=[Validator(is_type_of=int)])
+
+    def validator_condition(values):
+        if isinstance(_type, type) and issubclass(_type, ty.DictValue):
+            for i, value in enumerate(values):
+                for _validator in validators:
+                    for k, v in _validator.messages.items():
+                        _validator.messages[k] = v.replace(
+                            "{name}", prefix + f"[{i}]." + "{name}"
+                        )
+                    _validator.validate(BaseDynaconfSettings(**value))
+        else:
+            for i, value in enumerate(values):
+                _settings = BaseDynaconfSettings()
+                _settings.set(prefix, value)
+                _validator = BaseValidator(
+                    prefix,
+                    is_type_of=_type,
+                )
+                for k, v in _validator.messages.items():
+                    _validator.messages[k] = v.replace(
+                        "{name}", "{name}" + f"[{i}]"
+                    )
+                _validator.validate(_settings)
+
+        # No validation error
+        return True
+
+    return validator_condition
+
+
+def dict_merge(d1: dict, d2: dict) -> dict:
+    """Takes two dictionaries d1 and d2 and merges their data
+
+    Example:
+
+        >>> d1 = {'name': 'John', 'profile': {'group': 1, 'team': 42}}
+        >>> d2 = {'profile': {'username': 'jj', 'group': 2}}
+        >>> d3 = dict_merge(d1, d2)
+        >>> d3 == {'name': 'John', 'profile': {'group': 2, 'team': 42, 'username': 'jj'}}
+    """
+    if not isinstance(d2, dict):
+        return d1
+
+    merged = d1.copy()
+
+    for key, value in d2.items():
+        if (
+            key in merged
+            and isinstance(merged[key], dict)
+            and isinstance(value, dict)
+        ):
+            merged[key] = dict_merge(merged[key], value)
+        else:
+            merged[key] = value
+
+    return merged
+
+
+def dump_debug_info(init_options, validators):
+    """Debug utility to be removed later"""
+    dump = __builtins__["print"]  # cheat the linter
+
+    dump("\n----- INIT DEFAULTS -----")
+    __import__("pprint").pprint(init_options)
+    dump("\n----- VALIDATORS -----")
+    for i, validator in enumerate(validators, 1):
+        dump("#" * 80)
+        dump(validator.names[0].upper())
+        dump(i, validator)
+
+        def print_all_validators(v, prefix):
+            if v.items_validators:
+                dump(" " * len(prefix) + v.names[0].upper())
+                dump(" " * len(prefix) + ("-" * 80))
+            for n, item_v in enumerate(v.items_validators, 1):
+                dump(" " * len(prefix), f"{prefix}.{n}", item_v)
+                print_all_validators(item_v, f"{prefix}.{n}")
+
+        print_all_validators(validator, prefix=str(i))
diff --git a/dynaconf/utils/functional.py b/dynaconf/utils/functional.py
index c08fd12..f633279 100644
--- a/dynaconf/utils/functional.py
+++ b/dynaconf/utils/functional.py
@@ -5,7 +5,9 @@ import operator
 
 
 class Empty:
-    def __str__(self):
+    __slots__ = ()
+
+    def __repr__(self):
         return "EMPTY"
 
 
diff --git a/dynaconf/validator.py b/dynaconf/validator.py
index c0ab7d6..f5ceb3d 100644
--- a/dynaconf/validator.py
+++ b/dynaconf/validator.py
@@ -2,10 +2,13 @@ from __future__ import annotations
 
 from collections import defaultdict
 from collections.abc import Sequence
+from contextlib import suppress
+from copy import deepcopy
 from itertools import chain
 from types import MappingProxyType
 from typing import Any
 from typing import Callable
+from typing import get_args
 from typing import TYPE_CHECKING
 
 from dynaconf import validator_conditions
@@ -109,14 +112,6 @@ class Validator:
         }
     )
 
-    def __repr__(self):
-        return (
-            f"{self.__class__.__name__}("
-            f"{','.join(self.names)}, "
-            f"required={bool(self.must_exist)}, "
-            f"operations={self.operations})"
-        )
-
     def __init__(
         self,
         *names: str,
@@ -130,6 +125,7 @@ class Validator:
         default: Any | Callable[[Any, Validator], Any] | None = empty,
         description: str | None = None,
         apply_default_on_none: bool | None = False,
+        items_validators: list[Validator] | None = None,
         **operations: Any,
     ) -> None:
         # Copy immutable MappingProxyType as a mutable dict
@@ -153,15 +149,70 @@ class Validator:
         self.description = description
         self.envs: Sequence[str] | None = None
         self.apply_default_on_none = apply_default_on_none
-
-        # See #585
-        self.is_type_of = operations.get("is_type_of")
+        self.items_validators = items_validators or []
 
         if isinstance(env, str):
             self.envs = [env]
         elif isinstance(env, (list, tuple)):
             self.envs = env
 
+    def __repr__(self):
+        _repr = f"{self.__class__.__name__}("
+        _elements = []
+        if len(self.names) > 1:
+            _elements.append(f"{list(self.names)}")
+        elif self.names:
+            _elements.append(f"'{self.names[0]}'")
+        if self.is_type_of:
+            type_name = str(self.is_type_of)
+            if not get_args(self.is_type_of):
+                with suppress(Exception):
+                    type_name = (
+                        str(self.is_type_of)
+                        .replace("<class '", "")
+                        .replace("'>", "")
+                        .strip("<>'")
+                        .split(".")[-1]
+                    )
+            _elements.append(f"type={type_name}")
+        if self.must_exist is not None:
+            _elements.append(f"required={self.must_exist}")
+        operations = {
+            k: v for k, v in self.operations.items() if k != "is_type_of"
+        }
+        if operations:
+            _elements.append(f"operations={self.operations}")
+        if self.cast is not DEFAULT_CAST:
+            _elements.append(f"cast={self.cast}")
+        if self.condition:
+            _elements.append(f"condition={self.condition}")
+        if self.envs:
+            _elements.append(f"env={self.envs}")
+        if self.when:
+            _elements.append(f"when={self.when}")
+        if self.items_validators:
+            _elements.append(f"items_validators={self.items_validators}")
+        _repr += ", ".join(_elements)
+        _repr += ")"
+        return _repr
+
+    @property
+    def required(self) -> bool:
+        return bool(self.must_exist)
+
+    @required.setter
+    def required(self, value: bool):
+        self.must_exist = value
+
+    @property
+    def is_type_of(self):
+        # See #585
+        return self.operations.get("is_type_of")
+
+    @is_type_of.setter
+    def is_type_of(self, value):
+        self.operations["is_type_of"] = value
+
     def __or__(self, other: Validator) -> CombinedValidator:
         return OrValidator(self, other, description=self.description)
 
@@ -186,10 +237,11 @@ class Validator:
 
     def validate(
         self,
-        settings: Settings,
+        settings: Settings | dict,
         only: str | Sequence | None = None,
         exclude: str | Sequence | None = None,
         only_current_env: bool = False,
+        variable_path: tuple | None = None,
     ) -> None:
         """Raise ValidationError if invalid"""
         # If only or exclude are not set, this value always passes startswith
@@ -201,14 +253,15 @@ class Validator:
         if exclude and not isinstance(exclude[0], str):
             raise ValueError("'exclude' must be a string or list of strings.")
 
-        if self.envs is None:
-            self.envs = [settings.current_env]
+        current_env = getattr(settings, "current_env", "main")
+        envs = self.envs or [current_env]
 
+        # NOTE: Smells bad, must not mutate a validator
         if self.when is not None:
             try:
                 # inherit env if not defined
                 if self.when.envs is None:
-                    self.when.envs = self.envs
+                    self.when.envs = envs
 
                 self.when.validate(settings, only=only, exclude=exclude)
             except ValidationError:
@@ -216,38 +269,59 @@ class Validator:
                 return
 
         if only_current_env:
-            if settings.current_env.upper() in map(
-                lambda s: s.upper(), self.envs
-            ):
-                self._validate_items(
-                    settings, settings.current_env, only=only, exclude=exclude
+            if current_env.upper() in [s.upper() for s in envs]:
+                self._validate_names(
+                    settings,
+                    current_env,
+                    only=only,
+                    exclude=exclude,
+                    variable_path=variable_path,
                 )
             return
 
         # If only using current_env, skip using_env decoration (reload)
-        if (
-            len(self.envs) == 1
-            and self.envs[0].upper() == settings.current_env.upper()
-        ):
-            self._validate_items(
-                settings, settings.current_env, only=only, exclude=exclude
+        if len(envs) == 1 and envs[0].upper() == current_env.upper():
+            self._validate_names(
+                settings,
+                current_env,
+                only=only,
+                exclude=exclude,
+                variable_path=variable_path,
             )
             return
 
-        for env in self.envs:
-            env_settings: Settings = settings.from_env(env)
-            self._validate_items(env_settings, only=only, exclude=exclude)
+        for env in envs:
+            if from_env := getattr(settings, "from_env", None):
+                env_settings: Settings | dict = from_env(env)
+            else:
+                env_settings = settings
+
+            self._validate_names(
+                env_settings,
+                only=only,
+                exclude=exclude,
+                variable_path=variable_path,
+            )
             # merge source metadata into original settings for history inspect
-            settings._loaded_by_loaders.update(env_settings._loaded_by_loaders)
+            # use getattr to cheat mypy
+            if (d1 := getattr(settings, "_loaded_by_loaders", None)) and (
+                d2 := getattr(env_settings, "_loaded_by_loaders")
+            ):
+                d1.update(d2)
 
-    def _validate_items(
+    def _validate_names(
         self,
-        settings: Settings,
+        settings: Settings | dict,
         env: str | None = None,
         only: str | Sequence | None = None,
         exclude: str | Sequence | None = None,
+        variable_path: tuple | None = None,
     ) -> None:
-        env = env or settings.current_env
+        if env is None and (
+            current_env := getattr(settings, "current_env", None)
+        ):
+            env = current_env
+
         for name in self.names:
             # Skip if only is set and name isn't in the only list
             if only and not any(name.startswith(sub) for sub in only):
@@ -278,12 +352,20 @@ class Validator:
                 # avoid TOML from parsing "+-1" started strings as integer
                 default_value = f"'{default_value}'"
 
-            value = settings.setdefault(
-                name,
-                default_value,
-                apply_default_on_none=self.apply_default_on_none,
-                env=env,
-            )
+            # NOTE: must stop mutating settings here
+            if getattr(settings, "_store", None):
+                try:
+                    # settings is a Dynaconf instance
+                    value = getattr(settings, "setdefault")(  #  cheat mypy
+                        name,
+                        default_value,
+                        apply_default_on_none=self.apply_default_on_none,
+                        env=env,
+                    )
+                except AttributeError:  # dotted get/set on a non-dict type
+                    raise ValidationError(f"Mismatched type for {name}")
+            else:
+                value = settings.get(name, default_value)
 
             # is name required but not exists?
             if self.must_exist is True and value is empty:
@@ -305,22 +387,21 @@ class Validator:
             # by settings.setdefault above
             # however we need to cast it
             # so we call .set again
+            # NOTE: we must stop mutating settings here
             value = self.cast(settings.get(name))
-            settings.set(name, value, validate=False)
-
-            # is there a callable condition?
-            if self.condition is not None:
-                if not self.condition(value):
-                    _message = self.messages["condition"].format(
-                        name=name,
-                        function=self.condition.__name__,
-                        value=value,
-                        env=env,
-                    )
-                    raise ValidationError(_message, details=[(self, _message)])
+            if _set := getattr(settings, "set", None):
+                # Settings is Dynaconf
+                _set(name, value, validate=False)
+            else:
+                # settings is a dict
+                settings[name] = value
 
-            # operations
-            for op_name, op_value in self.operations.items():
+            # reorder passed operations so type check is made first
+            sorted_operations = sorted(
+                self.operations.items(),
+                key=lambda item: item[0] != "is_type_of",
+            )
+            for op_name, op_value in sorted_operations:
                 op_function = getattr(validator_conditions, op_name)
                 op_succeeded = False
 
@@ -335,13 +416,20 @@ class Validator:
                     # invalid type (not in __builtins__) may raise TypeError
                     try:
                         op_succeeded = op_function(value, op_value)
-                    except TypeError:
+                    except TypeError as e:
                         raise ValidationError(
                             f"Invalid type '{op_value}' for condition "
-                            "'is_type_of'. Should provide a valid type"
+                            f"'is_type_of'. Should provide a valid type. {e}"
                         )
                 else:
-                    op_succeeded = op_function(value, op_value)
+                    try:
+                        op_succeeded = op_function(value, op_value)
+                    except TypeError as e:
+                        raise ValidationError(
+                            f"Invalid Operation '{op_name}' "
+                            f"for type {type(value)!r} "
+                            f"on '{name}': {e}"
+                        )
 
                 if not op_succeeded:
                     _message = self.messages["operations"].format(
@@ -353,6 +441,67 @@ class Validator:
                     )
                     raise ValidationError(_message, details=[(self, _message)])
 
+            # is there a callable condition?
+            if self.condition is not None:
+                if not self.condition(value):
+                    _message = self.messages["condition"].format(
+                        name=name,
+                        function=self.condition.__name__,
+                        value=value,
+                        env=env,
+                    )
+                    raise ValidationError(_message, details=[(self, _message)])
+
+            # Type is list or dict and has internal validators
+            self._validate_internal_items(value, name, variable_path)
+
+    def _validate_internal_items(
+        self,
+        value: Any,
+        name: str,
+        variable_path: tuple | None = None,
+    ):
+        """Validate internal items of a data structure."""
+        variable_path = variable_path or tuple()
+        variable_path += (name,)
+        if isinstance(value, list):
+            for i, item in enumerate(value):
+                _item_path = tuple(variable_path)
+                if isinstance(item, dict):
+                    _item_path += (f"{i}", "{name}")
+                    _validator_location = ".".join(_item_path)
+                    data = item
+                else:
+                    _item_path += (f"{i}",)
+                    _validator_location = ".".join(_item_path)
+                    data = {"<item>": item}
+
+                for validator in self.items_validators:
+                    # avoid mutating a reusable validator
+                    if not validator.names:
+                        _validator = deepcopy(validator)
+                        _validator.names = ("<item>",)
+                    else:
+                        _validator = validator
+
+                    for k, v in _validator.default_messages.items():
+                        _validator.messages[k] = v.replace(
+                            "{name}", _validator_location
+                        )
+
+                    _validator.validate(data, variable_path=_item_path)
+
+        elif isinstance(value, dict):
+            _validator_location = ".".join(variable_path)
+            for validator in self.items_validators:
+                for k, v in validator.default_messages.items():
+                    validator.messages[k] = v.replace(
+                        "{name}", _validator_location + ".{name}"
+                    )
+                validator.validate(
+                    value, only=validator.names, variable_path=variable_path
+                )
+
 
 class CombinedValidator(Validator):
     def __init__(
@@ -378,6 +527,7 @@ class CombinedValidator(Validator):
         only: str | Sequence | None = None,
         exclude: str | Sequence | None = None,
         only_current_env: bool = False,
+        variable_path: tuple | None = None,
     ) -> None:  # pragma: no cover
         raise NotImplementedError(
             "subclasses OrValidator or AndValidator implements this method"
@@ -393,6 +543,7 @@ class OrValidator(CombinedValidator):
         only: str | Sequence | None = None,
         exclude: str | Sequence | None = None,
         only_current_env: bool = False,
+        variable_path: tuple | None = None,
     ) -> None:
         """Ensure at least one of the validators are valid"""
         errors = []
@@ -403,6 +554,7 @@ class OrValidator(CombinedValidator):
                     only=only,
                     exclude=exclude,
                     only_current_env=only_current_env,
+                    variable_path=variable_path,
                 )
             except ValidationError as e:
                 errors.append(e)
@@ -428,6 +580,7 @@ class AndValidator(CombinedValidator):
         only: str | Sequence | None = None,
         exclude: str | Sequence | None = None,
         only_current_env: bool = False,
+        variable_path: tuple | None = None,
     ) -> None:
         """Ensure both the validators are valid"""
         errors = []
@@ -438,6 +591,7 @@ class AndValidator(CombinedValidator):
                     only=only,
                     exclude=exclude,
                     only_current_env=only_current_env,
+                    variable_path=variable_path,
                 )
             except ValidationError as e:
                 errors.append(e)
@@ -514,7 +668,8 @@ class ValidatorList(list):
         only: str | Sequence | None = None,
         exclude: str | Sequence | None = None,
         only_current_env: bool = False,
-    ) -> None:
+        raise_error=True,
+    ) -> list[ValidationError]:
         errors = []
         details = []
         for validator in self:
@@ -530,7 +685,8 @@ class ValidatorList(list):
                 details.append((validator, str(e)))
                 continue
 
-        if errors:
+        if errors and raise_error:
             raise ValidationError(
                 "; ".join(str(e) for e in errors), details=details
             )
+        return errors
diff --git a/dynaconf/validator_conditions.py b/dynaconf/validator_conditions.py
index e4ad713..a7b6543 100644
--- a/dynaconf/validator_conditions.py
+++ b/dynaconf/validator_conditions.py
@@ -9,6 +9,13 @@ from typing import get_args
 from typing import get_origin
 from typing import Union
 
+# NOTE: Remove when dropping 3.9
+try:
+    from types import UnionType  # type: ignore
+except ImportError:
+    UnionType = Union  # type: ignore
+# /NOTE
+
 
 def eq(value, other):
     """Equal"""
@@ -46,11 +53,39 @@ def identity(value, other):
 
 
 def is_type_of(value, other):
-    """Type check"""
-    # Support Union[T, Y, Z] for python 3.9
-    # on python 3.10 isinstance can handle Union
-    if get_origin(other) is Union:
-        other = get_args(other)
+    """Type check that performs lookup on parameterized generic types.
+    For singular primary types this check is very fast ans just
+    calls isinstance.
+    For SpecialForm annotations like Union and Parametrized types like T[T]
+    or T[T, T] then this is a bit more costly.
+    NOTE: Find ways to improve the execution time on this, like
+    caching same value -> same type
+    """
+
+    origin = get_origin(other)
+    args = get_args(other)
+
+    if not args and isinstance(other, tuple):
+        # Got (T[T],) from recursive call
+        return any(is_type_of(value, item) for item in other)
+
+    if args:
+        if origin in [Union, UnionType]:
+            return any(is_type_of(value, arg) for arg in args)
+        elif origin in (list, tuple):
+            return isinstance(value, (list, tuple)) and all(
+                is_type_of(item, args) for item in value
+            )
+        elif origin is dict:
+            k_type, v_type = args
+            return isinstance(value, dict) and all(
+                is_type_of(k, k_type) and is_type_of(v, v_type)
+                for k, v in value.items()
+            )
+
+    if getattr(other, "_dynaconf_dictvalue", None):
+        other = dict
+
     return isinstance(value, other)
 
 


Test patch:
diff --git a/tests/test_typed.py b/tests/test_typed.py
index d3ce3ff..c3b09f3 100644
--- a/tests/test_typed.py
+++ b/tests/test_typed.py
@@ -1,13 +1,16 @@
 import os
+import sys
 from typing import Annotated
 from typing import Optional
 from typing import Union
 
 import pytest
 
+from dynaconf.typed import DictValue
 from dynaconf.typed import Dynaconf
 from dynaconf.typed import DynaconfSchemaError
-from dynaconf.typed import Nested
+from dynaconf.typed import ItemsValidator
+from dynaconf.typed import NotRequired
 from dynaconf.typed import Options
 from dynaconf.typed import ValidationError
 from dynaconf.typed import Validator
@@ -67,25 +70,13 @@ def test_options_contains_only_valid_and_set_attributes():
     }
 
 
-def test_enclosed_type_raises_for_defaults():
-    class Plugin(Nested):
-        name: str = "default_name"
-
-    class Settings(Dynaconf):
-        plugins: list[Plugin]
-
-    msg = "List enclosed types cannot define default values"
-    with pytest.raises(DynaconfSchemaError, match=msg):
-        Settings()
-
-
 def test_enclosed_type_raises_for_invalid_enclosed():
     class Dummy: ...
 
     class Settings(Dynaconf):
         plugins: list[Dummy]
 
-    msg = "Invalid enclosed type"
+    msg = "Invalid type 'Dummy'"
     with pytest.raises(DynaconfSchemaError, match=msg):
         Settings()
 
@@ -96,26 +87,27 @@ def test_union_enclosed_type_raises_for_invalid_enclosed():
     class Settings(Dynaconf):
         numbers: list[Union[int, float, bool, A]]
 
-    msg = "Invalid enclosed type 'A'"
+    msg = "Invalid type 'A'"
     with pytest.raises(DynaconfSchemaError, match=msg):
         Settings()
 
 
-def test_annotated_forbidden_with_nested():
-    class Plugin(Nested):
-        name: str
+def test_annotated_with_dictvalue():
+    class Plugin(DictValue):
+        name: Annotated[Union[str, int], Validator()]
 
     class Settings(Dynaconf):
-        # This is not allowed, should add validators on the nested type itself
-        plug: Annotated[Plugin, Validator()]
+        # This is not allowed, should add validators on the dictvalue type itself
+        plug: Annotated[Plugin, Validator(cont="name")]
 
-    msg = "Nested type cannot be Annotated"
-    with pytest.raises(DynaconfSchemaError, match=msg):
-        Settings()
+    msg = "plug.name must is_type_of.+Union\[str, int]"
+    with pytest.raises(ValidationError, match=msg):
+        Settings(plug={"name": 4.2})
 
 
+# NOT SURE ABOUT THIS
 def test_enclosed_forbidden_with_more_than_one_arg():
-    class Plugin(Nested):
+    class Plugin(DictValue):
         value: list[Union[int, float, bool], int, float]
 
     class Settings(Dynaconf):
@@ -127,7 +119,7 @@ def test_enclosed_forbidden_with_more_than_one_arg():
 
 
 def test_enclosed_type_validates(monkeypatch):
-    class Plugin(Nested):
+    class Plugin(DictValue):
         name: str
 
     class Settings(Dynaconf):
@@ -135,7 +127,7 @@ def test_enclosed_type_validates(monkeypatch):
 
     with monkeypatch.context() as m:
         m.setenv("DYNACONF_PLUGINS", '@json [{"name": true}]')
-        msg = r"plugins\[].name must is_type_of"
+        msg = r"plugins.0.name must is_type_of"
         with pytest.raises(ValidationError, match=msg):
             Settings()
 
@@ -151,13 +143,49 @@ def test_union_type_validates(monkeypatch):
             Settings()
 
 
-def test_union_enclosed_type_validates(monkeypatch):
+@pytest.mark.skipif(sys.version_info < (3, 10), reason="not supported on 3.9")
+def test_new_union_validates(monkeypatch):
+    class Settings(Dynaconf):
+        number: int | float | bool
+
+    with monkeypatch.context() as m:
+        m.setenv("DYNACONF_NUMBER", "one")
+        msg = r"number must is_type_of"
+        with pytest.raises(ValidationError, match=msg):
+            Settings()
+
+
+def test_union_enclosed_type_validates_type(monkeypatch):
     class Settings(Dynaconf):
         numbers: list[Union[int, float, bool]]
 
     with monkeypatch.context() as m:
         m.setenv("DYNACONF_NUMBERS", '["banana"]')
-        msg = r"numbers\[] must is_type_of"
+        msg = "numbers must is_type_of list\[typing.Union\[int, float, bool]]"
+        with pytest.raises(ValidationError, match=msg):
+            Settings()
+
+
+def test_union_enclosed_type_validates_operation(monkeypatch):
+    class Settings(Dynaconf):
+        numbers: Annotated[list[Union[int, float, bool]], Validator(len_min=3)]
+
+    with monkeypatch.context() as m:
+        m.setenv("DYNACONF_NUMBERS", "[1]")
+        msg = "numbers must len_min 3"
+        with pytest.raises(ValidationError, match=msg):
+            Settings()
+
+
+def test_union_enclosed_type_validates_items_operations(monkeypatch):
+    class Settings(Dynaconf):
+        numbers: Annotated[
+            list[Union[int, float]], ItemsValidator(Validator(gt=3))
+        ]
+
+    with monkeypatch.context() as m:
+        m.setenv("DYNACONF_NUMBERS", "[1]")
+        msg = r"numbers.0 must gt 3 but it is 1"
         with pytest.raises(ValidationError, match=msg):
             Settings()
 
@@ -172,31 +200,76 @@ def test_union_enclosed_type_works(monkeypatch):
         assert settings.numbers == [1, 4.2, True]
 
 
+def test_union_enclosed_type_raises_validation(monkeypatch):
+    class Settings(Dynaconf):
+        numbers: list[Union[int, float, bool]]
+
+    with monkeypatch.context() as m:
+        m.setenv("DYNACONF_NUMBERS", "[1, 4.2, 'Batata']")
+        msg = r"\b(numbers.0|must|is_type_of|Union|int|float|bool|Batata)\b"
+        with pytest.raises(ValidationError, match=msg):
+            Settings()
+
+
 def test_deeply_enclosed_type_validates(monkeypatch):
-    class Plugin(Nested):
+    class Plugin(DictValue):
         name: str
 
-    class Extra(Nested):
+    class Extra(DictValue):
         plugins: list[Plugin]
 
-    class Database(Nested):
+    class Database(DictValue):
+        extra: Extra
+
+    class Settings(Dynaconf):
+        database: Database
+
+    with monkeypatch.context() as m:
+        m.setenv("DYNACONF_DATABASE__EXTRA__PLUGINS", '@json [{"name": 42}]')
+        msg = r"database.extra.plugins.0.name must is_type_of"
+        with pytest.raises(ValidationError, match=msg):
+            Settings()
+
+
+def test_deeply_enclosed_type_validates_with_bare_dict(monkeypatch):
+    class Extra(DictValue):
+        plugins: list[dict]
+
+    class Database(DictValue):
+        extra: Extra
+
+    class Settings(Dynaconf):
+        database: Database
+
+    with monkeypatch.context() as m:
+        m.setenv("DYNACONF_DATABASE__EXTRA__PLUGINS", "[1]")
+        msg = r"database.extra.plugins must is_type_of list\[dict]"
+        with pytest.raises(ValidationError, match=msg):
+            Settings()
+
+
+def test_deeply_enclosed_type_validates_with_parameterized_dict(monkeypatch):
+    class Extra(DictValue):
+        plugins: list[dict[str, int]]
+
+    class Database(DictValue):
         extra: Extra
 
     class Settings(Dynaconf):
         database: Database
 
     with monkeypatch.context() as m:
-        m.setenv("DYNACONF_DATABASE__EXTRA__PLUGINS", '@json [{"name": null}]')
-        msg = r"database.extra.plugins\[].name must is_type_of"
+        m.setenv("DYNACONF_DATABASE__EXTRA__PLUGINS", '@json [{"name": 4.3}]')
+        msg = r"database.extra.plugins must is_type_of list\[dict\[str, int]]"
         with pytest.raises(ValidationError, match=msg):
             Settings()
 
 
 def test_deeply_enclosed_type_validates_with_bare_type(monkeypatch):
-    class Extra(Nested):
+    class Extra(DictValue):
         plugins: list[str]
 
-    class Database(Nested):
+    class Database(DictValue):
         extra: Extra
 
     class Settings(Dynaconf):
@@ -204,7 +277,7 @@ def test_deeply_enclosed_type_validates_with_bare_type(monkeypatch):
 
     with monkeypatch.context() as m:
         m.setenv("DYNACONF_DATABASE__EXTRA__PLUGINS", "@json [12]")
-        msg = r"database.extra.plugins\[] must is_type_of"
+        msg = r"database.extra.plugins must is_type_of list\[str]"
         with pytest.raises(ValidationError, match=msg):
             Settings()
 
@@ -219,10 +292,10 @@ def test_deeply_enclosed_type_validates_with_bare_type_allows_empty(
     https://mypy-play.net/?gist=d16da1574497dbe585eab679f09432ef
     """
 
-    class Extra(Nested):
+    class Extra(DictValue):
         plugins: list[str]
 
-    class Database(Nested):
+    class Database(DictValue):
         extra: Extra
 
     class Settings(Dynaconf):
@@ -234,18 +307,16 @@ def test_deeply_enclosed_type_validates_with_bare_type_allows_empty(
 
 
 def test_sub_types_works(monkeypatch):
-    class Plugin(Nested):
+    class Plugin(DictValue):
         name: str
-        path: Optional[
-            str
-        ]  # NOTE: This must mark path as not required on dict
-        order: Union[int, bool, None]  # This is the same as above ^
+        # path: NotRequired[str]
+        # order: Union[int, bool, None]
 
-    class Auth(Nested):
+    class Auth(DictValue):
         username: str
         password: int
 
-    class Extra(Nested):
+    class Extra(DictValue):
         log: bool = True
         socket_type: Annotated[int, Validator(is_in=[1, 2, 3])] = 1
         transactions: bool
@@ -254,7 +325,7 @@ def test_sub_types_works(monkeypatch):
         # Make this work
         batatas: list[int] = [1, 2]
 
-    class Database(Nested):
+    class Database(DictValue):
         host: str = "server.com"
         port: Annotated[int, Validator(gt=999)]
         engine: str = "postgres"
@@ -265,7 +336,7 @@ def test_sub_types_works(monkeypatch):
             envvar_prefix="MYTYPEDAPP",
         )
         version: Union[int, float, bool]
-        database: Database
+        database: Database = Database()
         batata: Annotated[int, Validator(gt=999)]
 
     with monkeypatch.context() as m:
@@ -289,7 +360,7 @@ def test_sub_types_works(monkeypatch):
 
 
 def test_sub_types_fail_validation(monkeypatch):
-    class Database(Nested):
+    class Database(DictValue):
         host: str = "server.com"
         port: Annotated[int, Validator(gt=999)]
         engine: str = "postgres"
@@ -310,29 +381,29 @@ def test_sub_types_fail_validation(monkeypatch):
             Settings()
 
 
-def test_deeply_nested_fail_validation(monkeypatch):
+def test_deeply_dictvalue_fail_validation(monkeypatch):
     """Ensure validation happens on deeply nested type"""
 
-    class Auth(Nested):
+    class Auth(DictValue):
         username: str
         password: int
 
-    class Extra(Nested):
+    class Extra(DictValue):
         log: bool = True
         socket_type: Annotated[int, Validator(is_in=[1, 2, 3])] = 1
-        auth: Auth
+        auth: Auth = Auth()
 
-    class Database(Nested):
+    class Database(DictValue):
         host: str = "server.com"
         port: Annotated[int, Validator(gt=999)]
         engine: str = "postgres"
-        extra: Extra
+        extra: Extra = Extra()
 
     class Settings(Dynaconf):
         dynaconf_options = Options(
             envvar_prefix="MYTYPEDAPP",
         )
-        database: Database
+        database: Database = Database()
         batata: Annotated[int, Validator(gt=999)]
 
     with monkeypatch.context() as m:
@@ -342,3 +413,830 @@ def test_deeply_nested_fail_validation(monkeypatch):
         error_msg = "database.extra.auth.username is required"
         with pytest.raises(ValidationError, match=error_msg):
             Settings()
+
+
+##### TYPING SCENARIOS #######
+
+
+# Use dynaconf_options to set Dynaconf initialization
+# NOTE: ADD more options for completeness
+def test_dynaconf_options(monkeypatch):
+    class Settings(Dynaconf):
+        dynaconf_options = Options(envvar_prefix="XPTO")
+        name: str
+
+    with monkeypatch.context() as m:
+        m.setenv("XPTO_NAME", "Bruno")
+        settings = Settings()
+        assert settings.name == "Bruno"
+
+    with monkeypatch.context() as m:
+        m.setenv("FOO_NAME", "Erik")
+        settings = Settings(envvar_prefix="FOO")
+        assert settings.name == "Erik"
+
+    settings = Settings(name="Karla")
+    assert settings.name == "Karla"
+
+
+# Set validation as immediate or lazy
+def test_eager_validation(monkeypatch):
+    class Settings(Dynaconf):
+        dynaconf_options = Options(envvar_prefix="XPTO")
+        name: Annotated[str, Validator(ne="Valdemort")]
+
+    with monkeypatch.context() as m:
+        m.setenv("XPTO_NAME", "Valdemort")
+        err_msg = "name must ne Valdemort"
+        with pytest.raises(ValidationError, match=err_msg):
+            Settings()
+
+
+def test_lazy_validation(monkeypatch):
+    class Settings(Dynaconf):
+        dynaconf_options = Options(
+            envvar_prefix="XPTO",
+            _trigger_validation=False,
+        )
+        name: Annotated[str, Validator(ne="Valdemort")]
+
+    with monkeypatch.context() as m:
+        m.setenv("XPTO_NAME", "Valdemort")
+        settings = Settings()
+        err_msg = "name must ne Valdemort"
+        with pytest.raises(ValidationError, match=err_msg):
+            settings.validators.validate()
+
+
+# Validate type based on annotated type `name: T`
+def test_validate_based_on_type_annotation(monkeypatch):
+    class Settings(Dynaconf):
+        dynaconf_options = Options(envvar_prefix="XPTO")
+        name: str
+
+    with monkeypatch.context() as m:
+        m.setenv("XPTO_NAME", "1234")
+        err_msg = "name must is_type_of .+str"
+        with pytest.raises(ValidationError, match=err_msg):
+            Settings()
+
+
+# Forbid `name: UnsupportedType`
+def test_only_supported_types_can_be_used():
+    class A: ...
+
+    class Settings(Dynaconf):
+        dynaconf_options = Options(_trigger_validation=False)
+        name: A
+
+    err_msg = "Invalid type 'A'"
+    with pytest.raises(DynaconfSchemaError, match=err_msg):
+        Settings()
+
+
+# handle dictvalue subtype
+def test_dictvalue_subtype():
+    class Person(DictValue):
+        name: str = "Bruno"
+        age: int
+
+    class Settings(Dynaconf):
+        person: Person = Person()
+
+    err_msg = "person.age is required"
+    with pytest.raises(ValidationError, match=err_msg):
+        Settings()
+
+
+# Set default based `name: T = default`
+def test_default_based_on_type_annotation(monkeypatch):
+    class Person(DictValue):
+        name: str = "Bruno"
+        age: int
+
+    class Settings(Dynaconf):
+        name: Annotated[str, Validator(ne="Valdemort")] = "John"
+        age: int = 45
+        colors: list[str] = ["red", "green", "blue"]
+        option: Union[int, float, bool] = 4.2
+        kwargs: dict[str, int] = {"foo": 3}
+        person: Person = Person(age=42)  # works with instance
+        # also works with dict overriding Person class defaults
+        other_person: Person = {"name": "Erik", "age": 5}
+        even_other_person: Person = {}
+        yet_another_person: Person
+
+    # arguments passed to init will combine with Person defaults
+    settings = Settings(
+        even_other_person={"age": 21},
+        yet_another_person={"name": "John", "age": 19},
+    )
+
+    assert settings.name == "John"
+    assert settings.age == 45
+    assert settings.colors == ["red", "green", "blue"]
+    assert settings.option == 4.2
+    assert settings.kwargs.foo == 3
+    assert settings.person.name == "Bruno"
+    assert settings.person.age == 42
+    assert settings.other_person.name == "Erik"
+    assert settings.other_person.age == 5
+    assert settings.even_other_person.name == "Bruno"
+    assert settings.even_other_person.age == 21
+    assert settings.yet_another_person.name == "John"
+    assert settings.yet_another_person.age == 19
+
+
+# Extract Validators from Annotated `field: Annotated[T, Validator()]`
+def test_extracted_validators_from_annotated(monkeypatch):
+    def cast_url(v):
+        return v.lower()
+
+    def auth_condition(v):
+        return "token" in v
+
+    class Auth(DictValue):
+        token: Annotated[str, Validator(len_min=10)]
+
+    not_banana = Validator(ne="banana")
+
+    class Settings(Dynaconf):
+        dynaconf_options = Options(
+            _trigger_validation=False,
+        )
+        name: Annotated[str, Validator(ne="Valdemort")]
+        age: int
+        team: str = "Default"
+        colors: Annotated[list, Validator(cont="red")]
+        port: Annotated[int, Validator(lt=1000), Validator(gt=10)]
+        url: Annotated[str, Validator(cast=cast_url)]
+        auth: Annotated[dict, Validator(condition=auth_condition)]
+        typed_auth: Auth
+        fruits: Annotated[list[str], ItemsValidator(not_banana)]
+
+    settings = Settings()
+
+    expected = [
+        {
+            "names": ("name",),
+            "operations": {"is_type_of": str},
+            "required": True,
+        },
+        {
+            "names": ("name",),
+            "operations": {"ne": "Valdemort"},
+        },
+        {
+            "names": ("age",),
+            "operations": {"is_type_of": int},
+            "required": True,
+        },
+        {
+            "names": ("team",),
+            "operations": {"is_type_of": str},
+        },
+        {
+            "names": ("colors",),
+            "operations": {"is_type_of": list},
+            "required": True,
+        },
+        {
+            "names": ("colors",),
+            "operations": {"cont": "red"},
+        },
+        {
+            "names": ("port",),
+            "operations": {"is_type_of": int},
+            "required": True,
+        },
+        {
+            "names": ("port",),
+            "operations": {"lt": 1000},
+        },
+        {
+            "names": ("port",),
+            "operations": {"gt": 10},
+        },
+        {
+            "names": ("url",),
+            "operations": {"is_type_of": str},
+            "required": True,
+        },
+        {
+            "names": ("url",),
+            "cast": cast_url,
+        },
+        {
+            "names": ("auth",),
+            "operations": {"is_type_of": dict},
+            "required": True,
+        },
+        {
+            "names": ("auth",),
+            "condition": auth_condition,
+        },
+        {
+            "names": ("typed_auth",),
+            "operations": {"is_type_of": Auth},
+            "required": True,
+        },
+        {
+            "names": ("fruits",),
+            "operations": {"is_type_of": list[str]},
+            "required": True,
+        },
+        {
+            "names": ("fruits",),
+            "items_validators": (not_banana,),
+        },
+    ]
+
+    for i, validator in enumerate(settings.validators):
+        for key, val in expected[i].items():
+            attr = getattr(validator, key)
+            if key == "operations":
+                for item in val.items():
+                    assert item in tuple(attr.items())
+            else:
+                assert attr == val
+
+    errors = settings.validators.validate_all(raise_error=False)
+    for i, exp in enumerate(item for item in expected if item.get("required")):
+        assert exp["names"][0] in str(errors[i])
+
+    settings = Settings(
+        name="Valdemort",
+        age=4.1,
+        team=True,
+        colors={"red": "green"},
+        port=5,
+        url="SERVER.com",
+        auth={"username": "foo"},
+        typed_auth={"token": []},
+        fruits=["apple", "banana"],
+    )
+    errors = settings.validators.validate_all(raise_error=False)
+    expected_errors = [
+        "name must ne Valdemort",
+        "age must is_type_of <class 'int'> but it is 4.1",
+        "team must is_type_of <class 'str'> but it is True",
+        "colors must is_type_of <class 'list'> but it is {'red': 'green'}",
+        "port must gt 10 but it is 5",
+        "auth invalid for auth_condition({'username': 'foo'})",
+        "typed_auth.token must is_type_of <class 'str'> but it is []",
+        "fruits.1 must ne banana",
+        "typed_auth.token must len_min 10",
+    ]
+    for i, error in enumerate(errors):
+        assert expected_errors[i] in str(error)
+
+
+# Forbid validators with defaults,is_type_of,names,must_exist,required
+@pytest.mark.parametrize(
+    "invalid_arg",
+    [
+        {"required": True},
+        {"must_exist": True},
+        {"is_type_of": int},
+        {"default": 42},
+        ("name",),
+    ],
+    ids=lambda value: str(value),
+)
+def test_some_arguments_forbid_on_validators(invalid_arg):
+    """these arguments doesnt make sense on a typed dynaconf"""
+    with pytest.raises(TypeError):
+        Validator(**invalid_arg)
+
+
+# Forbid Annotated with types out of the allowed list
+def test_annotated_accepts_only_supported_types():
+    class A: ...
+
+    class Settings(Dynaconf):
+        name: Annotated[A, Validator()]
+
+    msg = "Invalid type 'A'"
+    with pytest.raises(DynaconfSchemaError, match=msg):
+        Settings(name=A())
+
+    class Settings(Dynaconf):
+        name: Annotated[complex, Validator()]
+
+    msg = "Invalid type 'complex'"
+    with pytest.raises(DynaconfSchemaError, match=msg):
+        Settings()
+
+
+# Forbid args to Annotated that are not Validator `Annotated[T, strange_thing]`
+def test_annotated_args_must_be_only_validators():
+    # OK
+    class Settings(Dynaconf):
+        name: Annotated[str, Validator()] = "John"
+
+    Settings()
+
+    # NOT OK
+    class Settings(Dynaconf):
+        name: Annotated[str, Validator(), "Hello", 123] = "John"
+
+    msg = "Invalid Annotated Args"
+    with pytest.raises(DynaconfSchemaError, match=msg):
+        Settings()
+
+
+# Handle Union types `field: Union[T, T, T]`
+def test_all_kinds_of_union():
+    class Person(DictValue):
+        name: str
+
+    class Settings(Dynaconf):
+        person: Union[Person, str, dict]
+
+    assert Settings(person="Bruno").person == "Bruno"
+    assert Settings(person={"name": "Bruno"}).person.name == "Bruno"
+    assert Settings(person=Person()).person == {}
+    assert Settings(person=Person(name="Bruno")).person.name == "Bruno"
+
+    with pytest.raises(ValidationError, match="person must is_type_of"):
+        Settings(person=1)
+
+
+# Handle Optional types `field: Optional[T]` and `field: Union[T, None]`
+def test_optional_types():
+    """Optional and Union[T, None] must define a None default"""
+
+    class Profile(DictValue):
+        username: str
+
+    # Good
+    class Settings(Dynaconf):
+        name: Optional[str] = None
+        age: Optional[int] = None
+        colors: Optional[list] = None
+        profile: Optional[Profile] = None
+        city: Union[str, None] = None
+        team: Annotated[Optional[str], Validator()] = None
+        work: NotRequired[dict]
+
+    Settings(work={}, team="A")
+
+    # Not Good
+    class Settings(Dynaconf):
+        name: Optional[str]  # Should have used NotRequired
+
+    msg = "Optional Union must assign `None` explicitly as default."
+    with pytest.raises(DynaconfSchemaError, match=msg):
+        Settings()
+
+
+# Handle NotRequired
+def test_notrequired():
+    class Settings(Dynaconf):
+        name: NotRequired[str]
+        name2: Annotated[NotRequired[str], Validator()]
+        name3: NotRequired[Annotated[str, Validator()]]  # same as above
+
+    # No error as none of the variables are required
+    settings = Settings()
+
+    # However the NotRequired attribute might be undefined
+    with pytest.raises(AttributeError):
+        settings.name
+
+    # Same on SubTypes
+    class Person(DictValue):
+        name: str
+        age: NotRequired[int]
+        team: NotRequired[Annotated[str, Validator(is_in=["A", "B"])]]
+
+    class Settings(Dynaconf):
+        person: Person
+        person2: NotRequired[Person]
+        something: NotRequired[list]
+
+    # Good, partial dict because some fields are NotRequired
+    Settings(person={"name": "Bruno"})
+
+    # Validates for types and checks if a not required field is informed
+    with pytest.raises(
+        ValidationError,
+        match="person.age must is_type_of.+int",
+    ):
+        Settings(person={"name": "Bruno", "age": "too-old"})
+
+    with pytest.raises(
+        ValidationError, match="person.team must is_in \['A', 'B']"
+    ):
+        Settings(person={"name": "Bruno", "team": "C"})
+
+    with pytest.raises(
+        ValidationError,
+        match="something must is_type_of.+list",
+    ):
+        Settings(person={"name": "Bruno", "team": "A"}, something=5.6)
+
+
+# Handle list enclosed types `field: list[T]`  (and with default)
+# Handle empty list, must be allowed `field: list[T]` accepts `[]`
+def test_list_enclosed_type_is_required():
+    # simple and required
+    class Settings(Dynaconf):
+        colors: list[str]
+
+    Settings(colors=[])
+    Settings(colors=["a"])
+
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=1)
+    with pytest.raises(
+        ValidationError,
+        match="colors is required",
+    ):
+        Settings()
+
+
+# Forbid list enclosed with more than one arg `list[T, T]`
+@pytest.mark.xfail
+def test_list_enclosed_type_with_multi_args():
+    ...
+
+    # simple and required with multi args
+    # I THINK WE MUST ADD SUPPORT FOR IT
+    # but now it raises error
+    class Settings(Dynaconf):
+        colors: list[str, float]
+
+    Settings(colors=[])
+    Settings(colors=["a"])
+    Settings(colors=[4.2])
+
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=1)
+    with pytest.raises(
+        ValidationError,
+        match="colors is required",
+    ):
+        Settings()
+
+
+# Handle list enclosed union `field: list[Union[T, T, T]]`
+def test_list_enclosed_type_with_union():
+    # Unionized and required
+    class Settings(Dynaconf):
+        colors: list[Union[str, int]]
+
+    Settings(colors=[])
+    Settings(colors=["a"])
+    Settings(colors=[1])
+
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=1)
+    with pytest.raises(
+        ValidationError,
+        match="colors is required",
+    ):
+        Settings()
+
+
+def test_list_enclosed_type_outer_union():
+    """
+    As an alternative to list[T, T] one can use
+    Union[list[T], list[T]]
+    """
+
+    # Outer Unionized and required
+    class Settings(Dynaconf):
+        colors: Union[list[str], str]
+
+    Settings(colors=[])
+    Settings(colors=["a"])
+    Settings(colors="a")
+
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=1)
+    with pytest.raises(
+        ValidationError,
+        match="colors is required",
+    ):
+        Settings()
+
+
+def test_list_enclosed_type_annotated():
+    # Annotated with Validator
+    class Settings(Dynaconf):
+        dynaconf_options = Options(_trigger_validation=False)
+        colors: Annotated[list[str], Validator(cont="red")]
+
+    Settings(colors=["red"])
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=1).validators.validate()
+    with pytest.raises(
+        ValidationError,
+        match="Invalid Operation 'cont' for type <class 'int'> on 'colors'",
+    ):
+        Settings(colors=1).validators.validate_all()
+    with pytest.raises(
+        ValidationError,
+        match="colors must cont red",
+    ):
+        Settings(colors=[]).validators.validate()
+
+
+def test_list_enclosed_type_annotated_with_union():
+    # Annotated and Unionized
+    class Settings(Dynaconf):
+        colors: Annotated[Union[list[str], str], Validator(cont="blue")]
+
+    Settings(colors=["blue"])
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=1)
+    with pytest.raises(
+        ValidationError,
+        match="colors must cont blue",
+    ):
+        Settings(colors=[])
+
+
+@pytest.mark.skipif(sys.version_info < (3, 10), reason="not supported on 3.9")
+def test_list_enclosed_type_annotated_with_new_union():
+    # Annotated and Unionized
+    class Settings(Dynaconf):
+        colors: Annotated[list[str] | str, Validator(cont="blue")]
+
+    Settings(colors=["blue"])
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=1)
+    with pytest.raises(
+        ValidationError,
+        match="colors must cont blue",
+    ):
+        Settings(colors=[])
+
+
+def test_list_enclosed_type_annotated_with_union_nested():
+    # Annotated and Unionized with Nesting Union (python will flatten it)
+    class Settings(Dynaconf):
+        colors: Annotated[
+            Union[list[Union[str, int]], str], Validator(cont="green")
+        ]
+
+    Settings(colors=["green"])
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=1)
+    with pytest.raises(
+        ValidationError,
+        match="colors must cont green",
+    ):
+        Settings(colors=[])
+
+
+def test_list_enclosed_type_with_more_complex_unions():
+    # More combinations with Unions
+    class Settings(Dynaconf):
+        colors: Annotated[
+            Union[list[Union[str, int]], str, Union[str, int]],
+            Validator(cont="purple"),
+        ]
+
+    Settings(colors=["purple"])
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=4.2)
+    with pytest.raises(
+        ValidationError,
+        match="colors must cont purple",
+    ):
+        Settings(colors=[])
+
+
+def test_list_enclosed_type_crazy_type():
+    # A type that makes no sense but still validates
+    crazy_type = Union[
+        list[Union[str, int]], str, Union[list[str], tuple[Union[int, float]]]
+    ]
+
+    class Settings(Dynaconf):
+        colors: Annotated[crazy_type, Validator(cont="pink")]
+
+    Settings(colors=["pink"])
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list",
+    ):
+        Settings(colors=4.2)
+    with pytest.raises(
+        ValidationError,
+        match="colors must cont pink",
+    ):
+        Settings(colors=[])
+
+
+def test_list_enclosed_type_annotated_notrequired():
+    # NotRequired
+    class Settings(Dynaconf):
+        colors: Annotated[NotRequired[list[str]], Validator(cont="yellow")]
+
+    Settings()
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list\[str]",
+    ):
+        Settings(colors=[1, 2, 3])
+    with pytest.raises(
+        ValidationError,
+        match="colors must cont yellow",
+    ):
+        Settings(colors=["red"])
+
+
+def test_list_enclosed_type_notrequired():
+    # NotRequired
+    class Settings(Dynaconf):
+        colors: NotRequired[list[str]]
+
+    Settings()
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+list\[str]",
+    ):
+        Settings(colors=[1, 2, 3])
+
+
+def test_list_enclosed_type_with_default():
+    # With Default Value
+    crazy_type = Union[
+        list[Union[str, int]], str, Union[list[str], tuple[Union[int, float]]]
+    ]
+
+    class Settings(Dynaconf):
+        colors: Annotated[crazy_type, Validator(cont="cyan")] = ["cyan", 1]
+
+    assert Settings().colors == ["cyan", 1]
+
+
+# Handle list enclosed optional `field: list[Optional[T]]`
+def test_list_enclosed_type_with_optional():
+    class Settings(Dynaconf):
+        colors: list[Optional[str]]  # Means list can be [], [None], [str]
+
+    Settings(colors=[])
+    Settings(colors=[None, None])
+    Settings(colors=["red"])
+    with pytest.raises(
+        ValidationError,
+        match="colors is required",
+    ):
+        Settings()
+
+    with pytest.raises(
+        ValidationError,
+        match="colors must is_type_of.+Optional\[str]",
+    ):
+        Settings(colors=["red", "#123", 3])
+
+
+def test_usage_of_dict_value():
+    class Service(DictValue):
+        name: str
+
+    class Kind(DictValue):
+        id: int
+        services: list[Service]
+
+    class Profile(DictValue):
+        username: str
+        group: int = 1
+        team: int = 42
+        kind: Kind
+
+    class Person(DictValue):
+        name: str = "batata"
+        active: bool
+        bla: NotRequired[float]
+        numbers: list[int]
+        age: Annotated[NotRequired[int], Validator(gt=90)]
+        profile: Profile = Profile()  # w/ defaults
+        # profile2: Profile  # No defaults
+        profile2: NotRequired[Profile]
+        code: Union[str, int, float]
+
+    bruno = Person(
+        name="Bruno",
+        code="XPTO",
+        numbers=[1, 2, 3],
+        active=False,
+        profile=Profile(
+            username="br", kind=Kind(id=9, services=[Service(name="A")])
+        ),
+    )
+    jj = Person(
+        name="jj",
+        code=1234,
+        numbers=[1, 7, 9],
+        active=True,
+        profile=Profile(
+            username="j", kind=Kind(id=3, services=[Service(name="B")])
+        ),
+    )
+    boss = Person(
+        name="Mike",
+        code=45.5,
+        numbers=[5, 6, 7],
+        active=False,
+        profile=Profile(
+            username="m", kind=Kind(id=56, services=[Service(name="C")])
+        ),
+    )
+
+    class Settings(Dynaconf):
+        dynaconf_options = Options(_trigger_validation=False)
+        # not covered
+        # thing: Union[int, str]
+        # person_union: Union[Person, str, None]
+        # people_optional: Optional[list[Person]] = None
+        # people_notrequired: NotRequired[list[Person]]
+        # team: Annotated[list[Person], Validator(len_min=1)]
+
+        # covered
+        number: int = 99
+        person: Person = {"profile": {"username": "aaa", "group": 2}}
+        contact: Annotated[Person, Validator(cont="name")]
+        person_optional: Optional[Person] = None
+        person_default: Person = Person(
+            name="Person With Defaults",
+            numbers=[3, 4],
+            code=5678,
+            active=False,
+            profile=Profile(
+                username="aa",
+                kind=Kind(
+                    id=9,
+                    services=[
+                        Service(name="A"),
+                        Service(name="B"),
+                    ],
+                ),
+            ),
+        )
+        person_notrequired: NotRequired[Person]
+        people: list[Person]
+        people_default: list[Person] = [jj, boss]
+
+    settings = Settings(
+        _debug_mode=True,
+        person=bruno,
+        other_number=14,
+        number="@get other_number",
+        contact=jj,
+        people=[bruno, jj, boss],
+        # team=[boss],
+        # person_optional={"name": 1},  # must raise type error
+        # person_notrequired={"name": 2},  # must raise type error
+        # person_notrequired={"name": "aaaa"},  # must raise type error
+    )
+
+    settings.validators.validate()
+
+    assert settings.number == 14
+    assert settings.person.name == "Bruno"
+    assert settings.person.profile.kind.services[0].name == "A"
+    assert settings.people[1].name == "jj"
+
+
+# Handle deeply dictvalue Subtype (multiple levels) with enclosed types
+# Handle tuple type `field: tuple[T, T, T]` + Annotated and enclosed
+# Handle Any type in all cases
+# Handle Totality and Allow Extra
+
+##### IDEAS  ###########
+
+# Move typed.py to typed/main.py
+# Move helpers to typed/utils.py
+# Define ALLOWED_TYPES as type aliases on typed/types.py